Repository navigation
search: basePath never rewrites the query's own location - #6837
Conversation
MeshOperations.Search deleted every namespace: from the query text before prefixing the base, so a query anchored at one space under a basePath at another became a free-text term read in the base's partition (coverage named the wrong partition), and -namespace:X became a search for X. A query's own location now stands when it lies under the base and is refused naming both when it does not; only a location-less query is scoped by the base. Refs #6828 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
🟡 Changes recommended
Query composition still alters quoted values and can lose base constraints for exclusions and OR expressions.
3 open findings
What changed in this PR
Addresses #6828’s search-location mismatch by preserving explicit query locations rather than rewriting them with basePath.
Changes:
- Preserves locations within the base and refuses conflicting locations.
- Adds regression tests for anchors, exclusions, and base-only searches.
- Documents the location-composition contract.
| File | Description |
|---|---|
| test/Memex.Portal.Shared.Test/SearchBasePathKeepsTheQuerysLocationTest.cs | Tests basePath composition behavior. |
| src/MeshWeaver.Mesh.Operations/MeshOperations.cs | Validates query locations against the base. |
| src/MeshWeaver.Documentation/Data/Architecture/SearchCoverageAndRefusal.md | Documents location preservation and refusal. |
🧠 Review effort: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| .Where(token => !token.Equals("namespace:", StringComparison.OrdinalIgnoreCase) | ||
| && !token.Equals("path:", StringComparison.OrdinalIgnoreCase))); | ||
| var scoped = StatesScope(trimmed) ? trimmed : $"{trimmed} scope:descendants".Trim(); | ||
| fullQuery = $"namespace:{resolvedBase} {scoped}".Trim(); |
There was a problem hiding this comment.
Agreed, and the root cause was in the parser, so it is fixed there in bb6b277: QueryParser now emits a NEGATED multi-value namespace (-namespace:A|B, -namespace:(A OR B)) directly as a namespace NotIn (...) filter and leaves the anchor and scope alone, the same way the single -namespace:X arm already did. Only the positive alternation is deferred and clears the path. This also fixes a bare namespace:X -namespace:X/A|X/B query outside the tool. Regression AMultiValueExclusionUnderABaseKeepsTheBase covers both list spellings (expects only the non-archived node, coverage [BaseBeta]). Both fail without the fix. Hosting.Test and Graph.Test query/namespace suites still pass (210 + 74).
Test Results 20 files - 6 20 suites - 6 1h 0m 36s ⏱️ +39s Results for commit dfa37bd. ± Comparison against base commit cab0ae9. This pull request removes 369 and adds 27 tests. Note that renamed tests count towards both.♻️ This comment has been updated with latest results. |
…e exclusion, refuse a location in only some OR branches (#6828) Review of #6837: - the empty namespace:/path: strip is quote-aware and leaves the rest of the text verbatim (a split-and-rejoin collapsed spaces inside a quoted value and dropped a namespace: written inside one); - QueryParser emits -namespace:A|B as a NotIn filter and leaves the anchor and scope alone (it used to clear them with the positive alternation, so the composed query lost the base); - a query keeps its own location under a basePath only when the whole expression is bounded by it; a location in one OR branch is refused. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…lusion, verbatim text (#6828) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
| // its right branch everywhere. The query keeps its own location only when the WHOLE | ||
| // expression is bounded by it (an anchor, or a namespace filter on every branch — | ||
| // the sufficiency predicate with the `partitions:all` flag set aside). | ||
| if (!(own with { CrossPartition = false }).IsSufficientlySpecified()) |
| else if (query[i] == '"') | ||
| quoted = !quoted; |
| if (!token.Equals("namespace:", StringComparison.OrdinalIgnoreCase) | ||
| && !token.Equals("path:", StringComparison.OrdinalIgnoreCase)) | ||
| result.Append(token); |

Refs #6828 (part 3 of the issue: a query anchored at one namespace reported coverage of another partition).
Root cause
MeshOperations.Search(the MCPsearchtool and the agents'Search) composed abasePathby deleting everynamespace:from the query text and prefixingnamespace:{base}. Sonamespace:<space B> name:"…"underbasePath: @<space A>became the FREE-TEXT term<space B>read inside A — a semantic search over A (on Postgres, flooded with transcripts mentioning the words), reportingcoverage.partitions: ["<space A>"]. The coverage was honest about where it read; the composition read in the wrong place.-namespace:Xbecame a search FOR X.Measured on a live portal before the fix:
namespace:Doc/Architecture name:"Search Coverage and Refusal"withbasePath: @<my home>→count: 3, truncated, three thread-transcript documents,coverage.partitions: ["<my home>"]; the same query without the basePath → the one page,coverage.partitions: ["doc"].Fix
A query that names its own location keeps it when it lies at or under the base (the narrower statement), and is refused naming both when it lies outside — no answer can honour both. Only a query that names no location of its own is scoped by the base (exclusions are filters, not locations; an empty
namespace:, the "children of the base" idiomMeshPluginTest.Search_WithBasePath_ScopesResultsuses, names nothing and is dropped).Tests
Memex.Portal.Shared.Test/SearchBasePathKeepsTheQuerysLocationTest(in-memory mesh, throughMeshOperations.Search):AQueryAnchoredOutsideTheBaseIsRefused_NotReadInTheBasesPartitionAnExclusionUnderABaseStillExcludesAQueryAnchoredUnderTheBaseKeepsItsOwnAnchorandAQueryWithNoLocationIsScopedByTheBase(controls: the base still scopes, an emptynamespace:still means the base's subtree)Negative control (
MeshOperations.csreverted to main, test unchanged):total: 4, failed: 2, succeeded: 2—failed …AQueryAnchoredOutsideTheBaseIsRefused_NotReadInTheBasesPartition:Expected "{"count":0,…,"coverage":{"scope":"partition","partitions":["BaseAlpha"],…},"results":[]}" to start with "Error:"(the issue's symptom exactly);failed …AnExclusionUnderABaseStillExcludes:Expected collection {"BaseBeta/Archive/Jane"} to equal {"BaseBeta/Deal/Jane"}.Fix restored:
Memex.Portal.Shared.Test~Searchtotal: 21, failed: 0.Independent of #6836 (the
@prefix /basePath:qualifier in the query text); together they cover parts 1 and 3.Recycle on deploy: none.
Pairs-with: none — private members only.
🤖 Generated with Claude Code