Skip to content

search: basePath never rewrites the query's own location - #6837

Merged
rbuergi merged 4 commits into
mainfrom
fix/6828-basepath-keeps-query-anchor
Oct 15, 2026
Merged

rbuergi merged 4 commits into
mainfrom
fix/6828-basepath-keeps-query-anchor

Conversation

@rbuergi

@rbuergi rbuergi commented Oct 15, 2026

Copy link
Copy Markdown
Contributor

Refs #6828 (part 3 of the issue: a query anchored at one namespace reported coverage of another partition).

Root cause

MeshOperations.Search (the MCP search tool and the agents' Search) composed a basePath by deleting every namespace: from the query text and prefixing namespace:{base}. So namespace:<space B> name:"…" under basePath: @<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), reporting coverage.partitions: ["<space A>"]. The coverage was honest about where it read; the composition read in the wrong place. -namespace:X became a search FOR X.

Measured on a live portal before the fix: namespace:Doc/Architecture name:"Search Coverage and Refusal" with basePath: @<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" idiom MeshPluginTest.Search_WithBasePath_ScopesResults uses, names nothing and is dropped).

Tests

Memex.Portal.Shared.Test/SearchBasePathKeepsTheQuerysLocationTest (in-memory mesh, through MeshOperations.Search):

  • AQueryAnchoredOutsideTheBaseIsRefused_NotReadInTheBasesPartition
  • AnExclusionUnderABaseStillExcludes
  • AQueryAnchoredUnderTheBaseKeepsItsOwnAnchor and AQueryWithNoLocationIsScopedByTheBase (controls: the base still scopes, an empty namespace: still means the base's subtree)

Negative control (MeshOperations.cs reverted 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 ~Search total: 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

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>
@rbuergi
rbuergi enabled auto-merge October 15, 2026 13:54
Copilot AI balanced review requested due to automatic review settings October 15, 2026 13:54

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 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.

Comment thread src/MeshWeaver.Mesh.Operations/MeshOperations.cs Outdated
.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();

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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).

Comment thread src/MeshWeaver.Mesh.Operations/MeshOperations.cs
@github-actions

github-actions Bot commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Test Results

    20 files   -   6      20 suites   - 6   1h 0m 36s ⏱️ +39s
12 409 tests  - 316  12 409 ✅  - 123  0 💤  - 193  0 ❌ ±0 
12 425 runs   - 316  12 425 ✅  - 123  0 💤  - 193  0 ❌ ±0 

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.

   --- End of inner exception stack trace ---
   --- End of inner exception stack trace ---)
   --- End of inner exception stack trace ---, expected: True)
   --- End of inner exception stack trace ---, isDenial: True)
 ---> (Inner Exception #1) MeshWeaver.Mesh.QueryProviderStalledException: Query provider(s) [pg] did not emit an Initial within the query fan-in's 16s bound for query 'nodeType:NodeType' (user 'system'). The merged Initial gates on EVERY provider, so this query has NO snapshot to answer with — it is reported as unavailable (retryable) rather than left hanging with no error. This is an availability failure, never a permission verdict: a consumer deciding access must fail CLOSED and say it could not establish the answer. Fix the stalled provider; never bump the consumer's timeout.<---
 ---> (Inner Exception #1) MeshWeaver.Messaging.Hub.Test.InfrastructureFaultTest+ProviderException (0x80004005): Failed to connect to 10.42.18.4:5432<---
 ---> (Inner Exception #1) System.ArgumentException: Value does not fall within the expected range.<---
 ---> (Inner Exception #1) System.InvalidOperationException: boom<---
 ---> (Inner Exception #1) System.InvalidOperationException: source B is misconfigured<---
…
Memex.Portal.Shared.Test.InstanceIdRulesMatchTheRegistryTest ‑ TheSetupHostAgreesWithTheRegistry(candidate: "0df4c808-d862-47c5-9410-3a79dcb47d62")
Memex.Portal.Shared.Test.PublishStorageFaultIsNotAConflictTest ‑ AStorageFault_IsA500_NamingTheError(fault: System.AggregateException: One or more errors occurred. (Access to the path '/data/modules/activation.d/MeshWeaver.Markdown.Export.json' is denied.)
 ---> System.UnauthorizedAccessException: Access to the path '/data/modules/activation.d/MeshWeaver.Markdown.Export.json' is denied.
   --- End of inner exception stack trace ---)
Memex.Portal.Shared.Test.PublishStorageFaultIsNotAConflictTest ‑ AStorageFault_IsA500_NamingTheError(fault: System.InvalidOperationException: landing faulted
 ---> System.UnauthorizedAccessException: Access to the path '/data/modules/activation.d/MeshWeaver.Markdown.Export.json' is denied.
   --- End of inner exception stack trace ---)
Memex.Portal.Shared.Test.SearchBasePathKeepsTheQuerysLocationTest ‑ ALocationInOnlyOneOrBranchIsRefusedUnderABase
Memex.Portal.Shared.Test.SearchBasePathKeepsTheQuerysLocationTest ‑ AMultiValueExclusionUnderABaseKeepsTheBase(exclusion: "-namespace:(BaseBeta/Archive OR BaseBeta/Old)")
Memex.Portal.Shared.Test.SearchBasePathKeepsTheQuerysLocationTest ‑ AMultiValueExclusionUnderABaseKeepsTheBase(exclusion: "-namespace:BaseBeta/Archive|BaseBeta/Old")
Memex.Portal.Shared.Test.SearchBasePathKeepsTheQuerysLocationTest ‑ AQueryAnchoredOutsideTheBaseIsRefused_NotReadInTheBasesPartition
Memex.Portal.Shared.Test.SearchBasePathKeepsTheQuerysLocationTest ‑ AQueryAnchoredUnderTheBaseKeepsItsOwnAnchor
Memex.Portal.Shared.Test.SearchBasePathKeepsTheQuerysLocationTest ‑ AQueryWithNoLocationIsScopedByTheBase
Memex.Portal.Shared.Test.SearchBasePathKeepsTheQuerysLocationTest ‑ AQuotedValueIsSearchedVerbatimUnderABase(name: "Jane  Q Example", expected: "BaseBeta/Spaced")
…

♻️ This comment has been updated with latest results.

rbuergi and others added 3 commits October 15, 2026 21:15
…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>
Copilot AI balanced review requested due to automatic review settings October 15, 2026 19:19
@meshweaver-cloud
meshweaver-cloud Bot disabled auto-merge October 15, 2026 19:19
@rbuergi
rbuergi enabled auto-merge October 15, 2026 19:19

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

Scope expansion can escape the base, and qualifier cleanup still mishandles single-quoted values and empty locations.

4 open findings
2 resolved since last review

🧠 Review effort: Balanced

// 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())
Comment on lines +1642 to +1643
else if (query[i] == '"')
quoted = !quoted;
Comment on lines +1647 to +1649
if (!token.Equals("namespace:", StringComparison.OrdinalIgnoreCase)
&& !token.Equals("path:", StringComparison.OrdinalIgnoreCase))
result.Append(token);
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.

2 participants