From f09ebca6f58c812b64cf9036294e5cef2c9c9f1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roland=20B=C3=BCrgi?= Date: Mon, 24 Aug 2026 13:10:59 +0200 Subject: [PATCH 1/3] A surface says what it is, instead of borrowing a name that filtered right MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The home screen listed the platform's own bookkeeping — every NodeType, ModuleDefinition, Partition and Role registration — next to the viewer's actual spaces. Those are in-memory declarations contributed by AddMeshNodes so the platform knows a type exists. They are what a create menu offers. Nobody browses them. The cause was a borrowed name. Every browsing surface passed `context:search`: the home catalog, the first-level union, a node's children list, the recent lists. None of them are the search box. They said it because `search` was the one context whose filtering withheld registrations — and each of them then STILL carried its own patch on top (`-nodeType:NodeType`, `-nodeType:Store/Plugin`), because a name you don't mean cannot say what you want. So the surfaces now say what they are: `is:content`, alongside the existing `is:main`. It withholds registrations, and it IMPLIES `context:content`, which is what keeps the existing exclusion machinery working — per-node ExcludeFromContext and, when the marked node is a NodeType definition, per-type, pushed into SQL by the database providers. An explicit `context:` still wins. Declaration belongs to whoever owns the thing, not to the surface: MeshNode carries the mark, and MeshNodeVisibilityExtensions makes it one call — `.HideFrom(...)`, `.HideFromContent()`, `.HideEverywhere()` — additive and idempotent, so a module and a deployment can each contribute without silently dropping the other's. That is the point: a home screen enumerating what to hide is wrong the moment anyone installs something it has not heard of. The 52 registration sites that declared {"search"} / {"search","create"} now also declare "content". That is not new policy — it is the same nodes staying hidden from the same surfaces, said in the surfaces' own terms. Without it, dropping the borrowed `context:search` would have REVEALED them. One list survives, and it is called out where it lives: `-nodeType:Store/Plugin -nodeType:Store/Catalog` on the content scopes. Those are DYNAMIC node types living in the Store partition, so marking them is a MeshWeaver.Plugins change landing after this; deleting the terms first would put every installed plugin's root back on the content list, duplicating its own app tile. Content tabs, as asked: All, then Pinned, Mine, Shared. Pinned and Shared stay conditional — a pin list with no pins and a path union with no paths both match nothing, and an always-present tab would be an always-empty one. Mine is unconditional: your own space is the one tab that is never empty for you. Co-Authored-By: Claude Opus 5 (1M context) --- clients/react/src/i18n/strings.de.json | 1 + clients/react/src/i18n/strings.en.json | 1 + src/MeshWeaver.AI/AiSettingsNodeType.cs | 2 +- src/MeshWeaver.AI/ModelProviderNodeType.cs | 4 +- src/MeshWeaver.AI/ModelTierNodeType.cs | 2 +- src/MeshWeaver.AI/ThreadComposerNodeType.cs | 2 +- src/MeshWeaver.AI/TokenUsage.cs | 2 +- .../PortalNodeType.cs | 2 +- .../WhatsNew/2026-08-24-content-visibility.md | 20 ++++ .../GitHubSyncConfiguration.cs | 10 +- src/MeshWeaver.Graph/CommentNodeType.cs | 2 +- .../Configuration/AccessAssignmentNodeType.cs | 2 +- .../ActivityLogSegmentNodeType.cs | 2 +- .../Configuration/ActivityNodeType.cs | 2 +- .../Configuration/ApiTokenNodeType.cs | 2 +- .../Configuration/AppNodeType.cs | 2 +- .../Configuration/CodeNodeType.cs | 2 +- .../Configuration/CompletionMemoryNodeType.cs | 2 +- .../Configuration/EaCredentialNodeType.cs | 2 +- .../Configuration/EmailNodeType.cs | 2 +- .../EventSubscriptionNodeType.cs | 2 +- .../Configuration/GlobalSettingsNodeType.cs | 4 +- .../GraphSubscriptionNodeType.cs | 2 +- .../Configuration/GroupMembershipNodeType.cs | 2 +- .../Configuration/HomeConfigNodeType.cs | 4 +- .../Configuration/InvitationNodeType.cs | 2 +- .../Configuration/KernelNodeType.cs | 2 +- .../Configuration/LicenseNodeType.cs | 2 +- .../Configuration/MeshDataSourceNodeType.cs | 2 +- .../MeshWeaverInstanceNodeType.cs | 6 +- .../Configuration/NodeTypeNodeType.cs | 2 +- .../Configuration/NotificationNodeType.cs | 2 +- .../NotificationSettingsNodeType.cs | 2 +- .../PartitionAccessPolicyNodeType.cs | 2 +- .../Configuration/PartitionNodeType.cs | 2 +- .../Configuration/RoleNodeType.cs | 2 +- .../Configuration/ScheduledActionNodeType.cs | 2 +- .../Configuration/ScopeNodeType.cs | 2 +- .../TeamsConversationNodeType.cs | 2 +- .../Configuration/TrackedChangeNodeType.cs | 2 +- .../Configuration/UserActivityNodeType.cs | 2 +- .../Configuration/UserNodeType.cs | 2 +- .../Configuration/VUserNodeType.cs | 2 +- src/MeshWeaver.Graph/MeshNodeLayoutAreas.cs | 5 +- .../UserActivityLayoutAreas.cs | 42 ++++++--- .../Query/StaticNodeQueryProvider.cs | 10 +- .../InstanceSyncConfiguration.cs | 2 +- src/MeshWeaver.Mesh.Contract/MeshContexts.cs | 40 ++++++++ .../MeshNodeVisibilityExtensions.cs | 49 ++++++++++ .../Query/ParsedQuery.cs | 4 +- .../Query/QueryParser.cs | 19 +++- .../Localization/strings.de.json | 1 + .../Localization/strings.en.json | 1 + .../PluginCatalogConfigurationExtensions.cs | 6 +- test/MeshWeaver.Graph.Test/HomeCatalogTest.cs | 5 +- test/MeshWeaver.Graph.Test/HomeTabsTest.cs | 17 ++-- .../PresentationSurfacesTest.cs | 9 +- .../ContentContextQueryTest.cs | 94 +++++++++++++++++++ 58 files changed, 340 insertions(+), 84 deletions(-) create mode 100644 src/MeshWeaver.Documentation/Data/WhatsNew/2026-08-24-content-visibility.md create mode 100644 src/MeshWeaver.Mesh.Contract/MeshContexts.cs create mode 100644 src/MeshWeaver.Mesh.Contract/MeshNodeVisibilityExtensions.cs create mode 100644 test/MeshWeaver.Hosting.Test/ContentContextQueryTest.cs diff --git a/clients/react/src/i18n/strings.de.json b/clients/react/src/i18n/strings.de.json index 2c9710aaaf..cd9b6639db 100644 --- a/clients/react/src/i18n/strings.de.json +++ b/clients/react/src/i18n/strings.de.json @@ -131,6 +131,7 @@ "menu.history": "Verlauf", "home.all": "Alle", "home.sharedWithMe": "Mit mir geteilt", + "home.mine": "Meine", "home.pinned": "Angeheftet", "home.apps": "Apps", "home.content": "Inhalte", diff --git a/clients/react/src/i18n/strings.en.json b/clients/react/src/i18n/strings.en.json index 1d3637985a..c1f42c6add 100644 --- a/clients/react/src/i18n/strings.en.json +++ b/clients/react/src/i18n/strings.en.json @@ -131,6 +131,7 @@ "menu.history": "History", "home.all": "All", "home.sharedWithMe": "Shared with me", + "home.mine": "Mine", "home.pinned": "Pinned", "home.apps": "Apps", "home.content": "Content", diff --git a/src/MeshWeaver.AI/AiSettingsNodeType.cs b/src/MeshWeaver.AI/AiSettingsNodeType.cs index 9f5af77350..497350b36d 100644 --- a/src/MeshWeaver.AI/AiSettingsNodeType.cs +++ b/src/MeshWeaver.AI/AiSettingsNodeType.cs @@ -57,7 +57,7 @@ public static TBuilder AddAiSettingsType(this TBuilder builder) where Name = "AI Settings", Icon = "/static/NodeTypeIcons/sparkle.svg", IsSatelliteType = false, - ExcludeFromContext = new HashSet { "search", "create" }, + ExcludeFromContext = new HashSet { "search", "create", "content" }, HubConfiguration = config => config .AddMeshDataSource(source => source .WithContentType()) diff --git a/src/MeshWeaver.AI/ModelProviderNodeType.cs b/src/MeshWeaver.AI/ModelProviderNodeType.cs index 2a7ff57c2d..793df49662 100644 --- a/src/MeshWeaver.AI/ModelProviderNodeType.cs +++ b/src/MeshWeaver.AI/ModelProviderNodeType.cs @@ -196,7 +196,7 @@ public static TBuilder AddModelProviderType(this TBuilder builder, IsSatelliteType = false, // Creatable: an admin can author a ModelProvider node directly in a space // (e.g. Systemorph/Provider/AzureFoundry). Still hidden from search. - ExcludeFromContext = new HashSet { "search" }, + ExcludeFromContext = new HashSet { "search", "content" }, HubConfiguration = config => config .AddMeshDataSource(source => source .WithContentType()) @@ -215,7 +215,7 @@ public static TBuilder AddModelProviderType(this TBuilder builder, { Name = "Model Provider Selection", IsSatelliteType = false, - ExcludeFromContext = new HashSet { "search", "create" }, + ExcludeFromContext = new HashSet { "search", "create", "content" }, HubConfiguration = config => config .AddMeshDataSource(source => source .WithContentType()) diff --git a/src/MeshWeaver.AI/ModelTierNodeType.cs b/src/MeshWeaver.AI/ModelTierNodeType.cs index 12d5b0bb7b..1f1d6b1f59 100644 --- a/src/MeshWeaver.AI/ModelTierNodeType.cs +++ b/src/MeshWeaver.AI/ModelTierNodeType.cs @@ -79,7 +79,7 @@ public static TBuilder AddModelTierType(this TBuilder builder, // Creatable and editable through the standard node editor — that is the point of making a // tier a node. Kept out of free-text search results (like ModelProvider) so the registry // does not clutter every query; the AI menu's Tiers catalog is where you browse them. - ExcludeFromContext = new HashSet { "search" }, + ExcludeFromContext = new HashSet { "search", "content" }, HubConfiguration = config => config .AddMeshDataSource(source => source .WithContentType()) diff --git a/src/MeshWeaver.AI/ThreadComposerNodeType.cs b/src/MeshWeaver.AI/ThreadComposerNodeType.cs index d63625825e..e95e1b7120 100644 --- a/src/MeshWeaver.AI/ThreadComposerNodeType.cs +++ b/src/MeshWeaver.AI/ThreadComposerNodeType.cs @@ -80,7 +80,7 @@ public static TBuilder AddThreadComposerType(this TBuilder builder) wh // path segment (_Thread) and the nodeType resolve to `threads`, so write and read agree and // the selection persists. Hidden from the create menu and search so users never hand-create one. IsSatelliteType = true, - ExcludeFromContext = new HashSet { "search", "create" }, + ExcludeFromContext = new HashSet { "search", "create", "content" }, HubConfiguration = config => config .AddMeshDataSource(source => source .WithContentType()) diff --git a/src/MeshWeaver.AI/TokenUsage.cs b/src/MeshWeaver.AI/TokenUsage.cs index 3f23461f3c..55816bdd47 100644 --- a/src/MeshWeaver.AI/TokenUsage.cs +++ b/src/MeshWeaver.AI/TokenUsage.cs @@ -112,7 +112,7 @@ public static TBuilder AddTokenUsageType(this TBuilder builder) where { Name = "Token Usage", IsSatelliteType = true, - ExcludeFromContext = new HashSet { "search", "create" }, + ExcludeFromContext = new HashSet { "search", "create", "content" }, HubConfiguration = config => config .AddMeshDataSource(source => source .WithContentType()) diff --git a/src/MeshWeaver.Blazor.Portal/PortalNodeType.cs b/src/MeshWeaver.Blazor.Portal/PortalNodeType.cs index dff4889f0b..97a40ba8d2 100644 --- a/src/MeshWeaver.Blazor.Portal/PortalNodeType.cs +++ b/src/MeshWeaver.Blazor.Portal/PortalNodeType.cs @@ -51,7 +51,7 @@ public static TBuilder AddPortalType(this TBuilder builder) where TBui { Name = "Portal Session", IsSatelliteType = true, - ExcludeFromContext = new HashSet { "search", "create" }, + ExcludeFromContext = new HashSet { "search", "create", "content" }, HubConfiguration = config => { config.TypeRegistry.AddAITypes(); diff --git a/src/MeshWeaver.Documentation/Data/WhatsNew/2026-08-24-content-visibility.md b/src/MeshWeaver.Documentation/Data/WhatsNew/2026-08-24-content-visibility.md new file mode 100644 index 0000000000..5a716fd4a5 --- /dev/null +++ b/src/MeshWeaver.Documentation/Data/WhatsNew/2026-08-24-content-visibility.md @@ -0,0 +1,20 @@ +--- +Name: A home screen that shows your things, not the platform's +Category: Fix +Description: Type and module registrations no longer appear next to your spaces, and the content tabs are back. +Icon: Sparkle +Order: -20260824 +--- + +# A home screen that shows your things, not the platform's + +The home screen's list used to include the platform's own bookkeeping — the registrations that tell +MeshWeaver a type or a module exists — sitting next to your actual spaces. They are not things you +can open, and they should never have been there. + +Anything can now say where it should not appear, and it is the thing itself that says it rather than +the home screen keeping a list of what to leave out. That matters because a list is wrong the moment +you install something it has not heard of. + +The content tabs are back too: **All**, then **Pinned** when you have pins, **Mine** for your own +space, and **Shared** when someone has shared something with you. diff --git a/src/MeshWeaver.GitSync/GitHubSyncConfiguration.cs b/src/MeshWeaver.GitSync/GitHubSyncConfiguration.cs index 30dabeac4b..f98b692652 100644 --- a/src/MeshWeaver.GitSync/GitHubSyncConfiguration.cs +++ b/src/MeshWeaver.GitSync/GitHubSyncConfiguration.cs @@ -144,7 +144,7 @@ public static TBuilder AddGitHubSyncTypes(this TBuilder builder) where { Name = "GitHub Credential", IsSatelliteType = false, - ExcludeFromContext = new HashSet { "search", "create" }, + ExcludeFromContext = new HashSet { "search", "create", "content" }, HubConfiguration = config => config .AddMeshDataSource(source => source.WithContentType()), }, @@ -152,7 +152,7 @@ public static TBuilder AddGitHubSyncTypes(this TBuilder builder) where { Name = "GitHub Sync Config", IsSatelliteType = false, - ExcludeFromContext = new HashSet { "search", "create" }, + ExcludeFromContext = new HashSet { "search", "create", "content" }, HubConfiguration = config => config .AddMeshDataSource(source => source.WithContentType()), }, @@ -163,7 +163,7 @@ public static TBuilder AddGitHubSyncTypes(this TBuilder builder) where { Name = "Pull Request", IsSatelliteType = true, - ExcludeFromContext = new HashSet { "search", "create" }, + ExcludeFromContext = new HashSet { "search", "create", "content" }, HubConfiguration = config => config .AddMeshDataSource(source => source.WithContentType()), }, @@ -173,7 +173,7 @@ public static TBuilder AddGitHubSyncTypes(this TBuilder builder) where { Name = "GitHub Issue", IsSatelliteType = true, - ExcludeFromContext = new HashSet { "search", "create" }, + ExcludeFromContext = new HashSet { "search", "create", "content" }, HubConfiguration = config => config .AddMeshDataSource(source => source.WithContentType()), }, @@ -185,7 +185,7 @@ public static TBuilder AddGitHubSyncTypes(this TBuilder builder) where { Name = "Build Completion", IsSatelliteType = true, - ExcludeFromContext = new HashSet { "search", "create" }, + ExcludeFromContext = new HashSet { "search", "create", "content" }, HubConfiguration = config => config .AddMeshDataSource(source => source.WithContentType()), }); diff --git a/src/MeshWeaver.Graph/CommentNodeType.cs b/src/MeshWeaver.Graph/CommentNodeType.cs index 63dfc8d852..1056b8f14b 100644 --- a/src/MeshWeaver.Graph/CommentNodeType.cs +++ b/src/MeshWeaver.Graph/CommentNodeType.cs @@ -68,7 +68,7 @@ public static TBuilder AddCommentType(this TBuilder builder) where TBu Name = "Comment", Icon = "/static/NodeTypeIcons/comment.svg", IsSatelliteType = true, - ExcludeFromContext = new HashSet { "search", "create" }, + ExcludeFromContext = new HashSet { "search", "create", "content" }, HubConfiguration = config => config .AddCommentNodeViews() .AddMeshDataSource(source => source.WithContentType()) diff --git a/src/MeshWeaver.Graph/Configuration/AccessAssignmentNodeType.cs b/src/MeshWeaver.Graph/Configuration/AccessAssignmentNodeType.cs index 0e0f45912d..3549aa6186 100644 --- a/src/MeshWeaver.Graph/Configuration/AccessAssignmentNodeType.cs +++ b/src/MeshWeaver.Graph/Configuration/AccessAssignmentNodeType.cs @@ -33,7 +33,7 @@ public static TBuilder AddAccessAssignmentType(this TBuilder builder) Name = "Access Assignment", Icon = "/static/NodeTypeIcons/shield.svg", IsSatelliteType = true, - ExcludeFromContext = new HashSet { "search", "create" }, + ExcludeFromContext = new HashSet { "search", "create", "content" }, HubConfiguration = config => config .AddAccessAssignmentViews() .AddMeshDataSource(source => source diff --git a/src/MeshWeaver.Graph/Configuration/ActivityLogSegmentNodeType.cs b/src/MeshWeaver.Graph/Configuration/ActivityLogSegmentNodeType.cs index 151986621d..f9e60f289a 100644 --- a/src/MeshWeaver.Graph/Configuration/ActivityLogSegmentNodeType.cs +++ b/src/MeshWeaver.Graph/Configuration/ActivityLogSegmentNodeType.cs @@ -50,7 +50,7 @@ public static TBuilder AddActivityLogSegmentType(this TBuilder builder { Name = "Activity Log Segment", IsSatelliteType = true, - ExcludeFromContext = new HashSet { "search", "create" }, + ExcludeFromContext = new HashSet { "search", "create", "content" }, HubConfiguration = config => config .AddMeshDataSource(source => source .WithContentType()) diff --git a/src/MeshWeaver.Graph/Configuration/ActivityNodeType.cs b/src/MeshWeaver.Graph/Configuration/ActivityNodeType.cs index cbbab0119f..c55b8c5315 100644 --- a/src/MeshWeaver.Graph/Configuration/ActivityNodeType.cs +++ b/src/MeshWeaver.Graph/Configuration/ActivityNodeType.cs @@ -50,7 +50,7 @@ public static TBuilder AddActivityType(this TBuilder builder) where TB { Name = "Activity", IsSatelliteType = true, - ExcludeFromContext = new HashSet { "search", "create" }, + ExcludeFromContext = new HashSet { "search", "create", "content" }, // Activity hubs host the kernel directly: SubmitCodeRequest etc. land here, // run inside this hub's action block, and write progress to the same // ActivityLog node via DataChangeRequest.Update on the local workspace. diff --git a/src/MeshWeaver.Graph/Configuration/ApiTokenNodeType.cs b/src/MeshWeaver.Graph/Configuration/ApiTokenNodeType.cs index 56f381dc7c..90bcd42752 100644 --- a/src/MeshWeaver.Graph/Configuration/ApiTokenNodeType.cs +++ b/src/MeshWeaver.Graph/Configuration/ApiTokenNodeType.cs @@ -65,7 +65,7 @@ public static TBuilder AddApiTokenType(this TBuilder builder) where TB // MainNode = userId on each token row + the per-user-partition own-scope // shortcut in RlsNodeValidator to gate Create/Read. IsSatelliteType = false, - ExcludeFromContext = new HashSet { "search", "create" }, + ExcludeFromContext = new HashSet { "search", "create", "content" }, HubConfiguration = config => config .AddApiTokenViews() .WithHandler(HandleValidateToken) diff --git a/src/MeshWeaver.Graph/Configuration/AppNodeType.cs b/src/MeshWeaver.Graph/Configuration/AppNodeType.cs index 6077b950f5..111fb253db 100644 --- a/src/MeshWeaver.Graph/Configuration/AppNodeType.cs +++ b/src/MeshWeaver.Graph/Configuration/AppNodeType.cs @@ -51,7 +51,7 @@ public static TBuilder AddAppType(this TBuilder builder) where TBuilde Name = "Installed App", Icon = "/static/NodeTypeIcons/puzzlepiece.svg", IsSatelliteType = false, - ExcludeFromContext = new HashSet { "search", "create" }, + ExcludeFromContext = new HashSet { "search", "create", "content" }, HubConfiguration = config => config .AddMeshDataSource(source => source .WithContentType()) diff --git a/src/MeshWeaver.Graph/Configuration/CodeNodeType.cs b/src/MeshWeaver.Graph/Configuration/CodeNodeType.cs index 416c9e9c57..a69f7b559e 100644 --- a/src/MeshWeaver.Graph/Configuration/CodeNodeType.cs +++ b/src/MeshWeaver.Graph/Configuration/CodeNodeType.cs @@ -56,7 +56,7 @@ public static TBuilder AddCodeType(this TBuilder builder) where TBuild Name = "Code", Icon = "/static/NodeTypeIcons/code.svg", IsSatelliteType = false, - ExcludeFromContext = new HashSet { "search", "create" }, + ExcludeFromContext = new HashSet { "search", "create", "content" }, HubConfiguration = config => config .AddMeshDataSource(source => source .WithContentType()) diff --git a/src/MeshWeaver.Graph/Configuration/CompletionMemoryNodeType.cs b/src/MeshWeaver.Graph/Configuration/CompletionMemoryNodeType.cs index a8c1c5d09d..05e5c29b28 100644 --- a/src/MeshWeaver.Graph/Configuration/CompletionMemoryNodeType.cs +++ b/src/MeshWeaver.Graph/Configuration/CompletionMemoryNodeType.cs @@ -29,7 +29,7 @@ public static TBuilder AddCompletionMemoryType(this TBuilder builder) Name = "Completion Memory", Icon = "/static/NodeTypeIcons/code.svg", IsSatelliteType = true, - ExcludeFromContext = new HashSet { "search", "create" }, + ExcludeFromContext = new HashSet { "search", "create", "content" }, HubConfiguration = config => config .AddMeshDataSource(source => source .WithContentType()) diff --git a/src/MeshWeaver.Graph/Configuration/EaCredentialNodeType.cs b/src/MeshWeaver.Graph/Configuration/EaCredentialNodeType.cs index ece5dd5ba6..ca27a92bbc 100644 --- a/src/MeshWeaver.Graph/Configuration/EaCredentialNodeType.cs +++ b/src/MeshWeaver.Graph/Configuration/EaCredentialNodeType.cs @@ -29,7 +29,7 @@ public static TBuilder AddEaCredentialType(this TBuilder builder) wher { Name = "EA Credential", Icon = "/static/NodeTypeIcons/key.svg", - ExcludeFromContext = new HashSet { "search", "create" }, + ExcludeFromContext = new HashSet { "search", "create", "content" }, HubConfiguration = config => config .AddMeshDataSource(source => source .WithContentType()) diff --git a/src/MeshWeaver.Graph/Configuration/EmailNodeType.cs b/src/MeshWeaver.Graph/Configuration/EmailNodeType.cs index 34d8eff7e5..70c210594e 100644 --- a/src/MeshWeaver.Graph/Configuration/EmailNodeType.cs +++ b/src/MeshWeaver.Graph/Configuration/EmailNodeType.cs @@ -36,7 +36,7 @@ public static TBuilder AddEmailType(this TBuilder builder) where TBuil { Name = "Email", Icon = "/static/NodeTypeIcons/message.svg", - ExcludeFromContext = new HashSet { "search", "create" }, + ExcludeFromContext = new HashSet { "search", "create", "content" }, HubConfiguration = config => config .AddMeshDataSource(source => source .WithContentType()) diff --git a/src/MeshWeaver.Graph/Configuration/EventSubscriptionNodeType.cs b/src/MeshWeaver.Graph/Configuration/EventSubscriptionNodeType.cs index e631826593..34cfbeffb6 100644 --- a/src/MeshWeaver.Graph/Configuration/EventSubscriptionNodeType.cs +++ b/src/MeshWeaver.Graph/Configuration/EventSubscriptionNodeType.cs @@ -49,7 +49,7 @@ public static TBuilder AddEventSubscriptionType(this TBuilder builder) { Name = "Event Subscription", Icon = "/static/NodeTypeIcons/clock.svg", - ExcludeFromContext = new HashSet { "search", "create" }, + ExcludeFromContext = new HashSet { "search", "create", "content" }, HubConfiguration = config => config .AddMeshDataSource(source => source .WithContentType()) diff --git a/src/MeshWeaver.Graph/Configuration/GlobalSettingsNodeType.cs b/src/MeshWeaver.Graph/Configuration/GlobalSettingsNodeType.cs index 331c9533db..a5e98e731f 100644 --- a/src/MeshWeaver.Graph/Configuration/GlobalSettingsNodeType.cs +++ b/src/MeshWeaver.Graph/Configuration/GlobalSettingsNodeType.cs @@ -65,7 +65,7 @@ public static TBuilder AddGlobalSettingsType(this TBuilder builder) wh NodeType = NodeType, Name = "Settings", State = MeshNodeState.Active, - ExcludeFromContext = new HashSet { "search", "create" }, + ExcludeFromContext = new HashSet { "search", "create", "content" }, }); builder.AddMeshNodes(CreatePolicy()); @@ -118,7 +118,7 @@ private static MeshNode CreatePolicy() => Name = "Global Settings", Icon = "/static/NodeTypeIcons/settings.svg", IsSatelliteType = false, - ExcludeFromContext = new HashSet { "search", "create" }, + ExcludeFromContext = new HashSet { "search", "create", "content" }, HubConfiguration = config => config .AddDefaultGlobalSettingsMenuItems() .AddLayout(layout => layout diff --git a/src/MeshWeaver.Graph/Configuration/GraphSubscriptionNodeType.cs b/src/MeshWeaver.Graph/Configuration/GraphSubscriptionNodeType.cs index 4f672cafdd..0175249487 100644 --- a/src/MeshWeaver.Graph/Configuration/GraphSubscriptionNodeType.cs +++ b/src/MeshWeaver.Graph/Configuration/GraphSubscriptionNodeType.cs @@ -29,7 +29,7 @@ public static TBuilder AddGraphSubscriptionType(this TBuilder builder) { Name = "Graph Subscription", Icon = "/static/NodeTypeIcons/mail.svg", - ExcludeFromContext = new HashSet { "search", "create" }, + ExcludeFromContext = new HashSet { "search", "create", "content" }, HubConfiguration = config => config .AddMeshDataSource(source => source .WithContentType()) diff --git a/src/MeshWeaver.Graph/Configuration/GroupMembershipNodeType.cs b/src/MeshWeaver.Graph/Configuration/GroupMembershipNodeType.cs index c0fa85484f..47a1d2d6a8 100644 --- a/src/MeshWeaver.Graph/Configuration/GroupMembershipNodeType.cs +++ b/src/MeshWeaver.Graph/Configuration/GroupMembershipNodeType.cs @@ -32,7 +32,7 @@ public static TBuilder AddGroupMembershipType(this TBuilder builder) w { Name = "Group Membership", Icon = "/static/NodeTypeIcons/person.svg", - ExcludeFromContext = new HashSet { "search", "create" }, + ExcludeFromContext = new HashSet { "search", "create", "content" }, HubConfiguration = config => config .AddGroupMembershipViews() .AddMeshDataSource(source => source diff --git a/src/MeshWeaver.Graph/Configuration/HomeConfigNodeType.cs b/src/MeshWeaver.Graph/Configuration/HomeConfigNodeType.cs index 897dbfb505..d57b681291 100644 --- a/src/MeshWeaver.Graph/Configuration/HomeConfigNodeType.cs +++ b/src/MeshWeaver.Graph/Configuration/HomeConfigNodeType.cs @@ -59,7 +59,7 @@ public static TBuilder AddHomeConfigType(this TBuilder builder) where NodeType = NodeType, State = MeshNodeState.Active, Content = new HomeConfig(), - ExcludeFromContext = new HashSet { "search", "create" }, + ExcludeFromContext = new HashSet { "search", "create", "content" }, }); return builder; } @@ -70,7 +70,7 @@ public static TBuilder AddHomeConfigType(this TBuilder builder) where Name = "Home Page", Icon = "/static/NodeTypeIcons/layout.svg", IsSatelliteType = false, - ExcludeFromContext = new HashSet { "search", "create" }, + ExcludeFromContext = new HashSet { "search", "create", "content" }, HubConfiguration = config => config .AddDefaultLayoutAreas() .AddMeshDataSource(source => source.WithContentType()) diff --git a/src/MeshWeaver.Graph/Configuration/InvitationNodeType.cs b/src/MeshWeaver.Graph/Configuration/InvitationNodeType.cs index d95daeeaed..d3ccb8992e 100644 --- a/src/MeshWeaver.Graph/Configuration/InvitationNodeType.cs +++ b/src/MeshWeaver.Graph/Configuration/InvitationNodeType.cs @@ -62,7 +62,7 @@ public static TBuilder AddInvitationType(this TBuilder builder) where { Name = "Invitation", Icon = "/static/NodeTypeIcons/message.svg", - ExcludeFromContext = new HashSet { "search", "create" }, + ExcludeFromContext = new HashSet { "search", "create", "content" }, HubConfiguration = config => config .AddMeshDataSource(source => source .WithContentType()) diff --git a/src/MeshWeaver.Graph/Configuration/KernelNodeType.cs b/src/MeshWeaver.Graph/Configuration/KernelNodeType.cs index ad38eae3a3..f02403a815 100644 --- a/src/MeshWeaver.Graph/Configuration/KernelNodeType.cs +++ b/src/MeshWeaver.Graph/Configuration/KernelNodeType.cs @@ -85,7 +85,7 @@ public MessageHubConfiguration ConfigureSubHub(MessageHubConfiguration config) { Name = "Kernel Session", IsSatelliteType = true, - ExcludeFromContext = new HashSet { "search", "create" }, + ExcludeFromContext = new HashSet { "search", "create", "content" }, HubConfiguration = config => config .AddKernelHandlers() }; diff --git a/src/MeshWeaver.Graph/Configuration/LicenseNodeType.cs b/src/MeshWeaver.Graph/Configuration/LicenseNodeType.cs index d79c0ce44f..07546248f9 100644 --- a/src/MeshWeaver.Graph/Configuration/LicenseNodeType.cs +++ b/src/MeshWeaver.Graph/Configuration/LicenseNodeType.cs @@ -55,7 +55,7 @@ public static TBuilder AddLicenseType(this TBuilder builder) where TBu { Name = "License Acceptance", IsSatelliteType = true, - ExcludeFromContext = new HashSet { "search", "create" }, + ExcludeFromContext = new HashSet { "search", "create", "content" }, HubConfiguration = config => config .AddMeshDataSource(source => source.WithContentType()) }; diff --git a/src/MeshWeaver.Graph/Configuration/MeshDataSourceNodeType.cs b/src/MeshWeaver.Graph/Configuration/MeshDataSourceNodeType.cs index b0a34d07c7..40aa8f80ec 100644 --- a/src/MeshWeaver.Graph/Configuration/MeshDataSourceNodeType.cs +++ b/src/MeshWeaver.Graph/Configuration/MeshDataSourceNodeType.cs @@ -38,7 +38,7 @@ public static TBuilder AddMeshDataSourceType(this TBuilder builder) wh Name = "Data Source", Icon = "/static/NodeTypeIcons/database.svg", IsSatelliteType = false, - ExcludeFromContext = new HashSet { "search", "create" }, + ExcludeFromContext = new HashSet { "search", "create", "content" }, HubConfiguration = config => config .AddMeshDataSourceViews() .AddMeshDataSource(source => source.WithContentType()) diff --git a/src/MeshWeaver.Graph/Configuration/MeshWeaverInstanceNodeType.cs b/src/MeshWeaver.Graph/Configuration/MeshWeaverInstanceNodeType.cs index 6498506693..cc040570ca 100644 --- a/src/MeshWeaver.Graph/Configuration/MeshWeaverInstanceNodeType.cs +++ b/src/MeshWeaver.Graph/Configuration/MeshWeaverInstanceNodeType.cs @@ -74,7 +74,7 @@ public static TBuilder AddMeshWeaverInstanceType(this TBuilder builder // Regular content type, like ApiToken: MainNode = the owning userId on each instance row // plus the per-user-partition own-scope shortcut in RlsNodeValidator gates Create/Read. IsSatelliteType = false, - ExcludeFromContext = new HashSet { "search", "create" }, + ExcludeFromContext = new HashSet { "search", "create", "content" }, HubConfiguration = config => config .AddMeshDataSource(source => source .WithContentType() @@ -87,7 +87,7 @@ public static TBuilder AddMeshWeaverInstanceType(this TBuilder builder { Name = "Registration Key", IsSatelliteType = false, - ExcludeFromContext = new HashSet { "search", "create" }, + ExcludeFromContext = new HashSet { "search", "create", "content" }, HubConfiguration = config => config .AddMeshDataSource(source => source .WithContentType() @@ -101,7 +101,7 @@ public static TBuilder AddMeshWeaverInstanceType(this TBuilder builder { Name = "Plugin Grant", IsSatelliteType = false, - ExcludeFromContext = new HashSet { "search", "create" }, + ExcludeFromContext = new HashSet { "search", "create", "content" }, HubConfiguration = config => config .AddMeshDataSource(source => source .WithContentType()) diff --git a/src/MeshWeaver.Graph/Configuration/NodeTypeNodeType.cs b/src/MeshWeaver.Graph/Configuration/NodeTypeNodeType.cs index 45a92a5af5..c26830b343 100644 --- a/src/MeshWeaver.Graph/Configuration/NodeTypeNodeType.cs +++ b/src/MeshWeaver.Graph/Configuration/NodeTypeNodeType.cs @@ -31,7 +31,7 @@ public static TBuilder AddNodeTypeType(this TBuilder builder) where TB { Name = "Node Type", Icon = "/static/NodeTypeIcons/code.svg", - ExcludeFromContext = new HashSet { "search" }, + ExcludeFromContext = new HashSet { "search", "content" }, HubConfiguration = config => config .AddMeshDataSource(source => source .WithContentType()) diff --git a/src/MeshWeaver.Graph/Configuration/NotificationNodeType.cs b/src/MeshWeaver.Graph/Configuration/NotificationNodeType.cs index d3d0363498..535bd7127f 100644 --- a/src/MeshWeaver.Graph/Configuration/NotificationNodeType.cs +++ b/src/MeshWeaver.Graph/Configuration/NotificationNodeType.cs @@ -31,7 +31,7 @@ public static TBuilder AddNotificationType(this TBuilder builder) wher { Name = "Notification", Icon = "/static/NodeTypeIcons/bell.svg", - ExcludeFromContext = new HashSet { "search", "create" }, + ExcludeFromContext = new HashSet { "search", "create", "content" }, HubConfiguration = config => config .AddNotificationViews() .AddMeshDataSource(source => source diff --git a/src/MeshWeaver.Graph/Configuration/NotificationSettingsNodeType.cs b/src/MeshWeaver.Graph/Configuration/NotificationSettingsNodeType.cs index 0701fcc5c8..36936a01c0 100644 --- a/src/MeshWeaver.Graph/Configuration/NotificationSettingsNodeType.cs +++ b/src/MeshWeaver.Graph/Configuration/NotificationSettingsNodeType.cs @@ -40,7 +40,7 @@ public static TBuilder AddNotificationSettingsType(this TBuilder build Name = "Notification Settings", Icon = "/static/NodeTypeIcons/bell.svg", IsSatelliteType = true, - ExcludeFromContext = new HashSet { "search", "create" }, + ExcludeFromContext = new HashSet { "search", "create", "content" }, HubConfiguration = config => config .AddMeshDataSource(source => source .WithContentType()) diff --git a/src/MeshWeaver.Graph/Configuration/PartitionAccessPolicyNodeType.cs b/src/MeshWeaver.Graph/Configuration/PartitionAccessPolicyNodeType.cs index 49af6fb019..a135be0ba1 100644 --- a/src/MeshWeaver.Graph/Configuration/PartitionAccessPolicyNodeType.cs +++ b/src/MeshWeaver.Graph/Configuration/PartitionAccessPolicyNodeType.cs @@ -32,7 +32,7 @@ public static TBuilder AddPartitionAccessPolicyType(this TBuilder buil { Name = "Partition Access Policy", Icon = "/static/NodeTypeIcons/shield.svg", - ExcludeFromContext = new HashSet { "search", "create" }, + ExcludeFromContext = new HashSet { "search", "create", "content" }, HubConfiguration = config => config .AddMeshDataSource(source => source .WithContentType()) diff --git a/src/MeshWeaver.Graph/Configuration/PartitionNodeType.cs b/src/MeshWeaver.Graph/Configuration/PartitionNodeType.cs index 67198762bf..b2da4acc1c 100644 --- a/src/MeshWeaver.Graph/Configuration/PartitionNodeType.cs +++ b/src/MeshWeaver.Graph/Configuration/PartitionNodeType.cs @@ -62,7 +62,7 @@ public IEnumerable GetStaticNodes() Name = "Partition", NodeType = NodeType, Icon = "/static/NodeTypeIcons/database.svg", - ExcludeFromContext = new HashSet { "create", "search" }, + ExcludeFromContext = new HashSet { "create", "search", "content" }, Content = new NodeTypeDefinition { DefaultNamespace = Namespace }, HubConfiguration = config => config .AddMeshDataSource(source => source diff --git a/src/MeshWeaver.Graph/Configuration/RoleNodeType.cs b/src/MeshWeaver.Graph/Configuration/RoleNodeType.cs index f89f25d9dc..048eee4cdd 100644 --- a/src/MeshWeaver.Graph/Configuration/RoleNodeType.cs +++ b/src/MeshWeaver.Graph/Configuration/RoleNodeType.cs @@ -37,7 +37,7 @@ public static TBuilder AddRoleType(this TBuilder builder) where TBuild { Name = "Role", Icon = "/static/NodeTypeIcons/shield.svg", - ExcludeFromContext = new HashSet { "search" }, + ExcludeFromContext = new HashSet { "search", "content" }, HubConfiguration = config => config .AddMeshDataSource(source => source .WithContentType()) diff --git a/src/MeshWeaver.Graph/Configuration/ScheduledActionNodeType.cs b/src/MeshWeaver.Graph/Configuration/ScheduledActionNodeType.cs index e2e5de8025..3222e2e553 100644 --- a/src/MeshWeaver.Graph/Configuration/ScheduledActionNodeType.cs +++ b/src/MeshWeaver.Graph/Configuration/ScheduledActionNodeType.cs @@ -51,7 +51,7 @@ public static TBuilder AddScheduledActionType(this TBuilder builder) w { Name = "Scheduled Action", Icon = "/static/NodeTypeIcons/clock.svg", - ExcludeFromContext = new HashSet { "search", "create" }, + ExcludeFromContext = new HashSet { "search", "create", "content" }, HubConfiguration = config => config .AddMeshDataSource(source => source .WithContentType()) diff --git a/src/MeshWeaver.Graph/Configuration/ScopeNodeType.cs b/src/MeshWeaver.Graph/Configuration/ScopeNodeType.cs index a777eb0b25..cf2abbe521 100644 --- a/src/MeshWeaver.Graph/Configuration/ScopeNodeType.cs +++ b/src/MeshWeaver.Graph/Configuration/ScopeNodeType.cs @@ -42,7 +42,7 @@ public static TBuilder AddScopeType(this TBuilder builder) where TBuil Name = "Scope", Icon = "/static/NodeTypeIcons/code.svg", IsSatelliteType = false, - ExcludeFromContext = new HashSet { "search", "create" }, + ExcludeFromContext = new HashSet { "search", "create", "content" }, HubConfiguration = config => config .AddMeshDataSource(source => source .WithContentType()) diff --git a/src/MeshWeaver.Graph/Configuration/TeamsConversationNodeType.cs b/src/MeshWeaver.Graph/Configuration/TeamsConversationNodeType.cs index c3583c843b..b7119a4037 100644 --- a/src/MeshWeaver.Graph/Configuration/TeamsConversationNodeType.cs +++ b/src/MeshWeaver.Graph/Configuration/TeamsConversationNodeType.cs @@ -29,7 +29,7 @@ public static TBuilder AddTeamsConversationType(this TBuilder builder) { Name = "Teams Conversation", Icon = "/static/NodeTypeIcons/chat.svg", - ExcludeFromContext = new HashSet { "search", "create" }, + ExcludeFromContext = new HashSet { "search", "create", "content" }, HubConfiguration = config => config .AddMeshDataSource(source => source .WithContentType()) diff --git a/src/MeshWeaver.Graph/Configuration/TrackedChangeNodeType.cs b/src/MeshWeaver.Graph/Configuration/TrackedChangeNodeType.cs index c9d3c165f3..c6242c2e28 100644 --- a/src/MeshWeaver.Graph/Configuration/TrackedChangeNodeType.cs +++ b/src/MeshWeaver.Graph/Configuration/TrackedChangeNodeType.cs @@ -52,7 +52,7 @@ public static TBuilder AddTrackedChangeType(this TBuilder builder) whe Name = "TrackedChange", Icon = "/static/NodeTypeIcons/document.svg", IsSatelliteType = true, - ExcludeFromContext = new HashSet { "search", "create" }, + ExcludeFromContext = new HashSet { "search", "create", "content" }, HubConfiguration = config => config .AddMeshDataSource(source => source .WithContentType()) diff --git a/src/MeshWeaver.Graph/Configuration/UserActivityNodeType.cs b/src/MeshWeaver.Graph/Configuration/UserActivityNodeType.cs index bc200d80ba..8312b7c3a9 100644 --- a/src/MeshWeaver.Graph/Configuration/UserActivityNodeType.cs +++ b/src/MeshWeaver.Graph/Configuration/UserActivityNodeType.cs @@ -50,7 +50,7 @@ public static TBuilder AddUserActivityType(this TBuilder builder) wher { Name = "User Activity", IsSatelliteType = true, - ExcludeFromContext = new HashSet { "search", "create" }, + ExcludeFromContext = new HashSet { "search", "create", "content" }, // Declarative storage routing: UserActivity instances persist to the // `user_activities` satellite table within their owning partition's schema // (the single-sourced replacement for the central _UserActivity→user_activities diff --git a/src/MeshWeaver.Graph/Configuration/UserNodeType.cs b/src/MeshWeaver.Graph/Configuration/UserNodeType.cs index d23afc764c..c1fd99c66e 100644 --- a/src/MeshWeaver.Graph/Configuration/UserNodeType.cs +++ b/src/MeshWeaver.Graph/Configuration/UserNodeType.cs @@ -103,7 +103,7 @@ public static TBuilder AddSelfRegistry(this TBuilder builder) where TB Name = "User", Icon = "/static/NodeTypeIcons/person.svg", NodeType = NodeType, - ExcludeFromContext = new HashSet { "search" }, + ExcludeFromContext = new HashSet { "search", "content" }, // Post-v10 design: User nodes live at the ROOT namespace (path={userId}), // each user gets their own per-user partition. The previous design parked // them under namespace="User" — now superseded; setting an empty default diff --git a/src/MeshWeaver.Graph/Configuration/VUserNodeType.cs b/src/MeshWeaver.Graph/Configuration/VUserNodeType.cs index 1fcb995fc8..92167cab59 100644 --- a/src/MeshWeaver.Graph/Configuration/VUserNodeType.cs +++ b/src/MeshWeaver.Graph/Configuration/VUserNodeType.cs @@ -47,7 +47,7 @@ public static TBuilder AddVUserType(this TBuilder builder) where TBuil Name = "Virtual User", Icon = "/static/NodeTypeIcons/person.svg", NodeType = NodeType, - ExcludeFromContext = new HashSet { "search" }, + ExcludeFromContext = new HashSet { "search", "content" }, Content = new NodeTypeDefinition { DefaultNamespace = "VUser", RestrictedToNamespaces = ["VUser"] }, HubConfiguration = config => config .AddMeshDataSource(source => source diff --git a/src/MeshWeaver.Graph/MeshNodeLayoutAreas.cs b/src/MeshWeaver.Graph/MeshNodeLayoutAreas.cs index 16ff5ad387..fa083299af 100644 --- a/src/MeshWeaver.Graph/MeshNodeLayoutAreas.cs +++ b/src/MeshWeaver.Graph/MeshNodeLayoutAreas.cs @@ -1017,7 +1017,10 @@ private static MeshSearchControl BuildCatalog(string nodePath, CatalogOptions o) var search = Controls.MeshSearch .WithTitle(o.Title) // Exclude NodeType definitions — they belong to type admin, not the instance catalog. - .WithHiddenQuery($"namespace:{nodePath}{scope} is:main context:search -nodeType:NodeType") + // is:content, not context:search — this lists a node's children for a person to browse. + // It said "search" only to borrow that context's registration filtering, and then still + // needed `-nodeType:NodeType` on top because borrowing does not say what you mean. + .WithHiddenQuery($"namespace:{nodePath}{scope} is:main is:content") .WithVisibleQuery(o.SearchTerm ?? "") .WithNamespace(nodePath) .WithPlaceholder(o.Placeholder) diff --git a/src/MeshWeaver.Graph/UserActivityLayoutAreas.cs b/src/MeshWeaver.Graph/UserActivityLayoutAreas.cs index b4fd6c2c8e..b524fe5a77 100644 --- a/src/MeshWeaver.Graph/UserActivityLayoutAreas.cs +++ b/src/MeshWeaver.Graph/UserActivityLayoutAreas.cs @@ -721,8 +721,8 @@ private static UiControl BuildOpenThreads(string nodePath, string nodeOwnerId) = /// otherwise list itself on its own home page. (leading-space /// -nodeType:… clauses) applies to BOTH legs — the Spaces tab's dedup. private static string FirstLevelUnion(string ownerId, string sortSuffix, string exclusions = "") => - $"namespace: is:main context:search -nodeType:User{exclusions} {sortSuffix}\n" + - $"namespace:{ownerId} is:main context:search{exclusions} {sortSuffix}"; + $"namespace: is:main is:content -nodeType:User{exclusions} {sortSuffix}\n" + + $"namespace:{ownerId} is:main is:content{exclusions} {sortSuffix}"; /// The catalog query for a scope + sort suffix: the first-level union (partition roots + the /// user's home children), or a cross-partition SUBTREE query (everything the viewer can read at every @@ -730,7 +730,7 @@ private static string FirstLevelUnion(string ownerId, string sortSuffix, string /// nodes are excluded in both shapes — a home page never lists the user's own root. private static string CatalogQuery(HomeCatalogScope scope, string ownerId, string sortSuffix, string exclusions = "") => scope == HomeCatalogScope.Subtree - ? $"is:main context:search -nodeType:User{exclusions} {sortSuffix}" + ? $"is:main is:content -nodeType:User{exclusions} {sortSuffix}" : FirstLevelUnion(ownerId, sortSuffix, exclusions); // The three user-selectable sort orders (the view-options "Sort by" dropdown). LAST ACCESSED: @@ -831,7 +831,7 @@ internal static MeshSearchControl BuildContentSection( // queries structurally cannot see it (they walk the viewer's own partition and the roots). // Folding it in is what let the separate "Shared with me" section go without losing it. var sharedLeg = sharedTargets is { Count: > 0 } - ? "\n" + $"path:{string.Join("|", sharedTargets)} is:main -nodeType:User{SpacesDedupExclusions}" + ? "\n" + $"path:{string.Join("|", sharedTargets)} is:main is:content -nodeType:User{SpacesDedupExclusions}" : string.Empty; scopes.Add(ContentScope("home.all", locale, cfg.DefaultSort, (_, suffix) => CatalogQuery(cfg.Scope, nodeOwnerId, suffix, SpacesDedupExclusions) + sharedLeg)); @@ -853,6 +853,21 @@ internal static MeshSearchControl BuildContentSection( : $"{pinnedBase} {suffix}")); } + // Mine — the viewer's OWN partition, which is the second leg of the All union standing on + // its own. Unconditional: your own space is the one tab that is never empty for you, and a + // tab that appears and disappears is worse than one that is occasionally short. + scopes.Add(ContentScope("home.mine", locale, cfg.DefaultSort, + (_, suffix) => $"namespace:{nodeOwnerId} is:main is:content{SpacesDedupExclusions} {suffix}")); + + // Shared — the cross-partition invitations (#385), which All folds in as a union leg. As a + // TAB it answers the different question ("what did someone give me?"), so it is worth its + // own place; but only when there is something in it, since the query is a path list and an + // empty list matches nothing at all. + if (sharedTargets is { Count: > 0 }) + scopes.Add(ContentScope("home.sharedWithMe", locale, HomeCatalogSort.LastModified, + (_, suffix) => + $"path:{string.Join("|", sharedTargets)} is:main is:content -nodeType:User{SpacesDedupExclusions} {suffix}")); + // ONE list that FANS OUT by the node's own type — Spaces, Clients, Courses, … — with the // biggest group first (GroupByFrequency), so the page opens on what the viewer actually // works with. That is what "Spaces" was reaching for and failing to say: the categories @@ -945,7 +960,7 @@ internal static MeshSearchControl BuildAppsBand(string nodeOwnerId, string? loca { if (visibleShared.Count == 0) return null; - var sharedBase = $"path:{string.Join("|", visibleShared)} is:main -nodeType:User{SpacesDedupExclusions}"; + var sharedBase = $"path:{string.Join("|", visibleShared)} is:main is:content -nodeType:User{SpacesDedupExclusions}"; return Controls.MeshSearch .WithTitle(LocalizationCatalog.Get("home.sharedWithMe", locale)) .WithHiddenQuery($"{sharedBase} {SortSuffixLastAccessed}\n{sharedBase} {SortSuffixLastModified}") @@ -1109,10 +1124,15 @@ internal static MeshNode BuildAppRecord(string ownerId, AppRecordSpec spec) => }, }; - /// Dedup for the Spaces and Shared-with-me scopes: anything living in the Store (and - /// therefore representable as an installed app — plugin covers, the store root) is EXCLUDED, - /// so an app appears exactly once, on the Apps scope. Only non-redundant items survive: the - /// user's own spaces, shared workspaces, and their top-level home items. + /// Dedup for the content scopes: anything living in the Store (and therefore + /// representable as an installed app — plugin covers, the store root) is EXCLUDED, so an app + /// appears exactly once, on the Apps scope. + /// 🚨 This is the ONE list left, and it should not be here either: these belong on their + /// own NodeType nodes as ExcludeFromContext: ["content"] — which is one + /// .HideFromContent() call — but Store/Plugin and Store/Catalog are + /// DYNAMIC node types living in the Store partition, so marking them is a MeshWeaver.Plugins + /// change, landing after this. Deleting the terms first would put every installed plugin's root + /// back on the content list, where it duplicates its own app tile. private const string SpacesDedupExclusions = " -nodeType:Store/Plugin -nodeType:Store/Catalog"; /// @@ -1474,7 +1494,7 @@ private static UiControl BuildRecentActivity(string ownerId) => private static UiControl BuildProfileItems(string ownerId) => Controls.MeshSearch .WithTitle("Items") - .WithHiddenQuery($"namespace:{ownerId} is:main context:search scope:descendants sort:LastModified-desc") + .WithHiddenQuery($"namespace:{ownerId} is:main is:content scope:descendants sort:LastModified-desc") .WithShowEmptyMessage(true) .WithRenderMode(MeshSearchRenderMode.Grouped) .WithSectionCounts(true) @@ -1515,7 +1535,7 @@ internal static UiControl BuildShowcase(string ownerId, IReadOnlyList? p // Fallback — the owner's recent public content (visibility-filtered), so the band is never bare. return Controls.MeshSearch - .WithHiddenQuery($"namespace:{ownerId} is:main context:search scope:descendants sort:LastModified-desc") + .WithHiddenQuery($"namespace:{ownerId} is:main is:content scope:descendants sort:LastModified-desc") .WithShowSearchBox(false) .WithShowEmptyMessage(true) .WithRenderMode(MeshSearchRenderMode.Flat) diff --git a/src/MeshWeaver.Hosting/Persistence/Query/StaticNodeQueryProvider.cs b/src/MeshWeaver.Hosting/Persistence/Query/StaticNodeQueryProvider.cs index ccda7445ab..b883aef20d 100644 --- a/src/MeshWeaver.Hosting/Persistence/Query/StaticNodeQueryProvider.cs +++ b/src/MeshWeaver.Hosting/Persistence/Query/StaticNodeQueryProvider.cs @@ -350,8 +350,14 @@ bool MatchesScope(MeshNode node) => frontierPaths != null } } - var isSearch = string.Equals(context, "search", StringComparison.OrdinalIgnoreCase); - if (!isSearch && hasQualifier) + // Registration nodes — the in-memory type/module/partition declarations AddMeshNodes + // contributes — are not content. A surface that lists things for a person says so with + // `is:content` and is not handed them; that is a statement each surface makes on its own + // query, rather than a central list of context names that has to be kept in step with + // every surface that exists. Without it the home screen was handed every NodeType, + // ModuleDefinition, Partition and Role row alongside the user's actual spaces. + var isSearch = string.Equals(context, MeshContexts.Search, StringComparison.OrdinalIgnoreCase); + if (!isSearch && parsed.IsContent != true && hasQualifier) { foreach (var node in _configNodes) { diff --git a/src/MeshWeaver.InstanceSync/InstanceSyncConfiguration.cs b/src/MeshWeaver.InstanceSync/InstanceSyncConfiguration.cs index 945b9d6aa0..74a26ff0f5 100644 --- a/src/MeshWeaver.InstanceSync/InstanceSyncConfiguration.cs +++ b/src/MeshWeaver.InstanceSync/InstanceSyncConfiguration.cs @@ -54,7 +54,7 @@ public static TBuilder AddInstanceSyncTypes(this TBuilder builder) whe { Name = "Instance Sync Config", IsSatelliteType = false, - ExcludeFromContext = new HashSet { "search", "create" }, + ExcludeFromContext = new HashSet { "search", "create", "content" }, HubConfiguration = config => config .AddMeshDataSource(source => source.WithContentType()), }); diff --git a/src/MeshWeaver.Mesh.Contract/MeshContexts.cs b/src/MeshWeaver.Mesh.Contract/MeshContexts.cs new file mode 100644 index 0000000000..79645e8159 --- /dev/null +++ b/src/MeshWeaver.Mesh.Contract/MeshContexts.cs @@ -0,0 +1,40 @@ +namespace MeshWeaver.Mesh; + +/// +/// The visibility contexts a query can declare with context:. +/// +/// A context is how a surface says what it is FOR, which is what lets a node opt out of it +/// declaratively via : {"search"} keeps a node out +/// of the search box while leaving it creatable, {"content"} keeps it out of the lists people +/// browse. Every query provider — static, Postgres, Cosmos — already honours that property, per node +/// and (when the marked node is a NodeType definition) per type. The only thing a surface owes is to +/// name itself honestly. +/// +/// The names are constants rather than literals so a module can state its own visibility next +/// to its own registration instead of being enumerated in a list somewhere else. See +/// MeshNodeVisibilityExtensions for the fluent form. +/// +public static class MeshContexts +{ + /// The search box. + public const string Search = "search"; + + /// Create menus and type pickers — the surfaces that are ABOUT node types. + public const string Create = "create"; + + /// The header / navigation chrome. + public const string Header = "header"; + + /// + /// Browsing CONTENT — the home screen's list, a node's children, any surface showing a person + /// things they can open. Implied by is:content, so a surface names itself once and gets + /// both halves: registration nodes are withheld, and anything marked + /// ExcludeFromContext: ["content"] stays out. + /// + /// Before this existed, those surfaces passed context:search — claiming to be the + /// search box, because that was the only context whose filtering happened to do what they + /// wanted. Borrowing a name you do not mean is why each of them ALSO carried its own + /// -nodeType:… patch on top. + /// + public const string Content = "content"; +} diff --git a/src/MeshWeaver.Mesh.Contract/MeshNodeVisibilityExtensions.cs b/src/MeshWeaver.Mesh.Contract/MeshNodeVisibilityExtensions.cs new file mode 100644 index 0000000000..2cb7a9e958 --- /dev/null +++ b/src/MeshWeaver.Mesh.Contract/MeshNodeVisibilityExtensions.cs @@ -0,0 +1,49 @@ +namespace MeshWeaver.Mesh; + +/// +/// Fluent visibility for a : where it should NOT show up. +/// +/// The point is that a module states this next to its own registration, and a deployment +/// states it in its own configuration — instead of every surface maintaining a list of the things +/// it does not want to see. A home screen that enumerates what to hide is wrong the moment anyone +/// installs anything it has not heard of. +/// +/// +/// // In a module's configuration: +/// .AddMeshNodes(new MeshNode("PluginRegistryCredential") { Name = "…" } +/// .HideFrom(MeshContexts.Search, MeshContexts.Create, MeshContexts.Content)) +/// +/// // In a deployment's final config (Systemorph/Memex), for something shipped by someone else: +/// .HideFromContent("Hosting", "Store/Plugin") +/// +/// +/// On a NodeType DEFINITION node the mark covers every instance of that type — one line hides +/// a whole family. On an ordinary node it covers just that node. +/// +public static class MeshNodeVisibilityExtensions +{ + /// Adds the given contexts to this node's . + /// Additive and idempotent, so a module and a deployment can each contribute without one + /// silently dropping the other's. + public static MeshNode HideFrom(this MeshNode node, params string[] contexts) + { + if (contexts is not { Length: > 0 }) + return node; + var merged = new HashSet( + node.ExcludeFromContext ?? Array.Empty(), StringComparer.OrdinalIgnoreCase); + foreach (var context in contexts) + if (!string.IsNullOrWhiteSpace(context)) + merged.Add(context); + return node with { ExcludeFromContext = merged }; + } + + /// Keeps this node out of the lists people browse — the home screen and node children + /// — while leaving search and create menus alone. . + public static MeshNode HideFromContent(this MeshNode node) => node.HideFrom(MeshContexts.Content); + + /// Infrastructure: a node that exists so the platform works, and that no one should + /// ever meet while browsing, searching, or creating. Credentials, signing keys, ledgers, + /// discovery records. + public static MeshNode HideEverywhere(this MeshNode node) => + node.HideFrom(MeshContexts.Search, MeshContexts.Create, MeshContexts.Content); +} diff --git a/src/MeshWeaver.Mesh.Contract/Query/ParsedQuery.cs b/src/MeshWeaver.Mesh.Contract/Query/ParsedQuery.cs index 499bb4c6ea..a3acaf4130 100644 --- a/src/MeshWeaver.Mesh.Contract/Query/ParsedQuery.cs +++ b/src/MeshWeaver.Mesh.Contract/Query/ParsedQuery.cs @@ -16,6 +16,7 @@ namespace MeshWeaver.Mesh; /// Context for visibility filtering (from context: qualifier) /// When true, filters to main nodes only (MainNode is null or equals Path) /// Multi-value path filter from path:a|b|c alternation. When set, backends should push down WHERE path IN (...). is also set to the first value for back-compat with consumers that don't know about multi-path. +/// From is:content — the caller is listing CONTENT, so REGISTRATION nodes are not wanted: the in-memory type definitions, module definitions and partition declarations that AddMeshNodes contributes exist so the platform knows a type exists, and are what a create menu or an autocomplete offers, not something a person browses. A surface that lists things for a human says so on its own query rather than being enumerated in a central list of context names somewhere else. public record ParsedQuery( QueryNode? Filter, string? TextSearch, @@ -27,7 +28,8 @@ public record ParsedQuery( IReadOnlyList? Select = null, string? Context = null, bool? IsMain = null, - IReadOnlyList? Paths = null + IReadOnlyList? Paths = null, + bool? IsContent = null ) { /// diff --git a/src/MeshWeaver.Mesh.Contract/Query/QueryParser.cs b/src/MeshWeaver.Mesh.Contract/Query/QueryParser.cs index 5c2c9793a0..551d4edb29 100644 --- a/src/MeshWeaver.Mesh.Contract/Query/QueryParser.cs +++ b/src/MeshWeaver.Mesh.Contract/Query/QueryParser.cs @@ -33,7 +33,7 @@ public ParsedQuery Parse(string? query) return ParsedQuery.Empty with { Paths = listPaths, Path = listPaths[0] }; var tokens = Tokenize(query); - var (filterTokens, textSearch, path, scope, orderBy, limit, source, select, context, isMain, paths) = ExtractReservedQualifiers(tokens); + var (filterTokens, textSearch, path, scope, orderBy, limit, source, select, context, isMain, paths, isContent) = ExtractReservedQualifiers(tokens); // Parse the filter expression from remaining tokens QueryNode? filter = null; @@ -43,7 +43,13 @@ public ParsedQuery Parse(string? query) filter = ParseOr(filterTokens, ref position); } - return new ParsedQuery(filter, textSearch, path, scope, orderBy, limit, source, select, context, isMain, paths); + // `is:content` IS a context — the one every browsing surface means. Defaulting it here is + // what keeps the existing exclusion machinery (per-node ExcludeFromContext, and per-type + // when the marked node is a NodeType definition, pushed into SQL by the DB providers) + // working unchanged, instead of these surfaces continuing to claim `context:search`. + // An explicit context: always wins. + context ??= isContent == true ? MeshContexts.Content : null; + return new ParsedQuery(filter, textSearch, path, scope, orderBy, limit, source, select, context, isMain, paths, isContent); } /// @@ -440,7 +446,7 @@ private string ParseSingleValue(string input, ref int i, bool stopAtParens = tru /// Extracts reserved qualifiers (path, namespace, scope, sort, limit, source) from tokens. /// Returns remaining filter tokens and extracted values. /// - private (List FilterTokens, string? TextSearch, string? Path, QueryScope Scope, OrderByClause? OrderBy, int? Limit, QuerySource Source, IReadOnlyList? Select, string? Context, bool? IsMain, IReadOnlyList? Paths) + private (List FilterTokens, string? TextSearch, string? Path, QueryScope Scope, OrderByClause? OrderBy, int? Limit, QuerySource Source, IReadOnlyList? Select, string? Context, bool? IsMain, IReadOnlyList? Paths, bool? IsContent) ExtractReservedQualifiers(List tokens) { var filterTokens = new List(); @@ -467,6 +473,7 @@ private string ParseSingleValue(string input, ref int i, bool stopAtParens = tru IReadOnlyList? select = null; string? context = null; bool? isMain = null; + bool? isContent = null; foreach (var token in tokens) { @@ -617,6 +624,10 @@ private string ParseSingleValue(string input, ref int i, bool stopAtParens = tru { if (value.Equals("main", StringComparison.OrdinalIgnoreCase)) isMain = true; + // is:content — "I am listing content for a person, so do not hand me + // registration nodes". The surface states its own need on its own query. + else if (value.Equals("content", StringComparison.OrdinalIgnoreCase)) + isContent = true; continue; } } @@ -675,7 +686,7 @@ private string ParseSingleValue(string input, ref int i, bool stopAtParens = tru } var textSearch = textSearchParts.Count > 0 ? string.Join(" ", textSearchParts) : null; - return (filterTokens, textSearch, path, scope, orderBy, limit, source, select, context, isMain, paths); + return (filterTokens, textSearch, path, scope, orderBy, limit, source, select, context, isMain, paths, isContent); } /// diff --git a/src/MeshWeaver.Messaging.Hub/Localization/strings.de.json b/src/MeshWeaver.Messaging.Hub/Localization/strings.de.json index 2c9710aaaf..cd9b6639db 100644 --- a/src/MeshWeaver.Messaging.Hub/Localization/strings.de.json +++ b/src/MeshWeaver.Messaging.Hub/Localization/strings.de.json @@ -131,6 +131,7 @@ "menu.history": "Verlauf", "home.all": "Alle", "home.sharedWithMe": "Mit mir geteilt", + "home.mine": "Meine", "home.pinned": "Angeheftet", "home.apps": "Apps", "home.content": "Inhalte", diff --git a/src/MeshWeaver.Messaging.Hub/Localization/strings.en.json b/src/MeshWeaver.Messaging.Hub/Localization/strings.en.json index 1d3637985a..c1f42c6add 100644 --- a/src/MeshWeaver.Messaging.Hub/Localization/strings.en.json +++ b/src/MeshWeaver.Messaging.Hub/Localization/strings.en.json @@ -131,6 +131,7 @@ "menu.history": "History", "home.all": "All", "home.sharedWithMe": "Shared with me", + "home.mine": "Mine", "home.pinned": "Pinned", "home.apps": "Apps", "home.content": "Content", diff --git a/src/MeshWeaver.PluginCatalog/PluginCatalogConfigurationExtensions.cs b/src/MeshWeaver.PluginCatalog/PluginCatalogConfigurationExtensions.cs index aab31a0f26..75d094aba6 100644 --- a/src/MeshWeaver.PluginCatalog/PluginCatalogConfigurationExtensions.cs +++ b/src/MeshWeaver.PluginCatalog/PluginCatalogConfigurationExtensions.cs @@ -215,7 +215,7 @@ public static ITypeRegistry AddPluginCatalogTypes(this ITypeRegistry typeRegistr { Name = "Plugin Registry Credential", IsSatelliteType = false, - ExcludeFromContext = new HashSet { "search", "create" }, + ExcludeFromContext = new HashSet { "search", "create", "content" }, HubConfiguration = config => config .AddMeshDataSource(s => s.WithContentType()), }; @@ -228,7 +228,7 @@ public static ITypeRegistry AddPluginCatalogTypes(this ITypeRegistry typeRegistr { Name = "Sync Token Signing Key", IsSatelliteType = false, - ExcludeFromContext = new HashSet { "search", "create" }, + ExcludeFromContext = new HashSet { "search", "create", "content" }, HubConfiguration = config => config .AddMeshDataSource(s => s.WithContentType()), }; @@ -267,7 +267,7 @@ private static MeshNode CreateDefaultInstallLedgerNodeType() => // Bookkeeping, not a satellite of anything: it is a single node in the Plugins // partition, addressed by a fixed path, exactly like the credential node type. IsSatelliteType = false, - ExcludeFromContext = new HashSet { "search", "create" }, + ExcludeFromContext = new HashSet { "search", "create", "content" }, HubConfiguration = config => config .AddMeshDataSource(s => s.WithContentType()), }; diff --git a/test/MeshWeaver.Graph.Test/HomeCatalogTest.cs b/test/MeshWeaver.Graph.Test/HomeCatalogTest.cs index 69cba85130..a573cc743e 100644 --- a/test/MeshWeaver.Graph.Test/HomeCatalogTest.cs +++ b/test/MeshWeaver.Graph.Test/HomeCatalogTest.cs @@ -193,7 +193,10 @@ public void Catalog_ConfigSubtree_QueriesTheWholeTree_NotJustFirstLevel() var search = UserActivityLayoutAreas.BuildCatalog(NodePath, cfg).Should().BeOfType().Subject; var query = search.HiddenQuery!.ToString()!; - query.Should().Contain("is:main context:search"); + // is:content, not context:search. The catalog lists content for a person to browse; it + // claimed to be the search box only to borrow that context's filtering. + query.Should().Contain("is:main is:content"); + query.Should().NotContain("context:search"); query.Should().NotContain("namespace:"); // no first-level roots/home-children union query.Should().NotContain("\n"); // a single query, not a union } diff --git a/test/MeshWeaver.Graph.Test/HomeTabsTest.cs b/test/MeshWeaver.Graph.Test/HomeTabsTest.cs index a5c716d776..00475b3e0c 100644 --- a/test/MeshWeaver.Graph.Test/HomeTabsTest.cs +++ b/test/MeshWeaver.Graph.Test/HomeTabsTest.cs @@ -64,14 +64,17 @@ public void Home_WithShares_StaysTwoSections_AndFoldsThemIntoAll() } [Fact] - public void Content_NoPins_IsOneCategory_NoTabStrip() + public void Content_NoPins_LeadsWithAll_AndKeepsMine() { - // "i am not sure if 'spaces' makes sense … it should just be all the top level nodes which - // we can access. make just one category." One scope ⇒ the view renders no strip at all. + // "tabs below: as discussed, include All, then Pinned, Mine, Shared". Pinned and Shared are + // conditional — a pin list with no pins and a path union with no paths both match nothing, + // so an always-present tab would be an always-empty one. Mine is unconditional: your own + // space is the one tab that is never empty for you. var content = Content(UserActivityLayoutAreas.BuildHome(NodePath)); - ScopeLabels(content).Should().Equal("All"); + ScopeLabels(content).Should().Equal("All", "Mine"); content.HiddenQuery!.ToString().Should().Contain("is:main") + .And.Contain("is:content", "the home lists content; it is not the search box") .And.Contain("-nodeType:Store/Plugin", "apps live in the Apps section, never twice"); } @@ -97,9 +100,9 @@ public void Content_WithPins_AllLeads_PinnedFollows() var content = UserActivityLayoutAreas.BuildContentSection( NodePath, null, new User { PinnedPaths = ["Doc/GUI"] }, null, null); - // "put All first and default tab": the view activates scopes[0], so All leads and Pinned - // is the narrower lens behind it. - ScopeLabels(content).Should().Equal("All", "Pinned"); + // "put All first and default tab": the view activates scopes[0], so All leads and the + // narrower lenses follow it, in the order asked for — Pinned, Mine, then Shared. + ScopeLabels(content).Should().Equal("All", "Pinned", "Mine"); content.ScopeTabs![1].Query.Should().Contain("Doc/GUI"); content.HiddenQuery!.ToString().Should().Be(content.ScopeTabs![0].Query, "the control-level fallback IS the default tab's query"); diff --git a/test/MeshWeaver.Graph.Test/PresentationSurfacesTest.cs b/test/MeshWeaver.Graph.Test/PresentationSurfacesTest.cs index b482753f30..6f1ea4ed58 100644 --- a/test/MeshWeaver.Graph.Test/PresentationSurfacesTest.cs +++ b/test/MeshWeaver.Graph.Test/PresentationSurfacesTest.cs @@ -106,11 +106,12 @@ public void Content_AScopeWhoseWholeContentIsMarked_Disappears() var user = new User { PinnedPaths = ["Acme/Q3-Renewal"] }; // Off: the Pinned tab is there. - ScopeLabels(ContentOf(user, null)).Should().Equal("All", "Pinned"); + ScopeLabels(ContentOf(user, null)).Should().Equal("All", "Pinned", "Mine"); // On, with Acme marked: everything pinned is inside the marked space, so the tab goes — an - // empty tab labelled "Pinned" would itself say something. - ScopeLabels(ContentOf(user, Active("Acme"))).Should().Equal("All"); + // empty tab labelled "Pinned" would itself say something. Mine is unaffected: it is scoped + // to the viewer's own partition, not to the marked one. + ScopeLabels(ContentOf(user, Active("Acme"))).Should().Equal("All", "Mine"); } [Fact] @@ -118,7 +119,7 @@ public void Content_AScopeKeepsItsUnmarkedContent() { var content = ContentOf(new User { PinnedPaths = ["Acme", "Doc/Guide"] }, Active("Acme")); - ScopeLabels(content).Should().Equal("All", "Pinned"); + ScopeLabels(content).Should().Equal("All", "Pinned", "Mine"); ScopeQuery(content, "Pinned").Should().NotContain("Acme").And.Contain("Doc/Guide"); } diff --git a/test/MeshWeaver.Hosting.Test/ContentContextQueryTest.cs b/test/MeshWeaver.Hosting.Test/ContentContextQueryTest.cs new file mode 100644 index 0000000000..64b71d0e0e --- /dev/null +++ b/test/MeshWeaver.Hosting.Test/ContentContextQueryTest.cs @@ -0,0 +1,94 @@ +using MeshWeaver.Mesh; +using Xunit; + +namespace MeshWeaver.Hosting.Test; + +/// +/// is:content — a surface saying what it is, instead of borrowing a name that happened to +/// filter the way it wanted. +/// +/// Every browsing surface used to pass context:search: the home screen, a node's +/// children list, the recent lists. None of them are the search box. They said it because that was +/// the one context whose filtering withheld registration nodes — and each of them then still needed +/// its own -nodeType:… patch on top, because a borrowed name does not say what you mean. +/// +/// The registration/content split is the substance: the in-memory type, module and partition +/// declarations AddMeshNodes contributes exist so the platform knows a type exists. They are +/// what a create menu offers. They are not things a person browses, and they should never have been +/// listed next to somebody's actual spaces. +/// +public class ContentContextQueryTest +{ + private static ParsedQuery Parse(string query) => new QueryParser().Parse(query); + + [Fact] + public void Is_content_sets_the_flag_AND_implies_the_content_context() + { + var parsed = Parse("is:main is:content namespace:acme"); + + parsed.IsContent.Should().BeTrue(); + parsed.IsMain.Should().BeTrue(); + // Implying the context is what keeps the EXISTING exclusion machinery working — per-node + // ExcludeFromContext, and per-type when the marked node is a NodeType definition, pushed + // into SQL by the database providers. Without it, dropping context:search from these + // surfaces would have REVEALED the infrastructure nodes it was quietly filtering. + parsed.Context.Should().Be(MeshContexts.Content); + } + + [Fact] + public void An_explicit_context_always_wins_over_the_implied_one() + { + Parse("is:content context:create").Context.Should().Be(MeshContexts.Create); + Parse("is:content context:search").Context.Should().Be(MeshContexts.Search); + } + + [Fact] + public void A_query_that_does_not_say_is_content_is_untouched() + { + var parsed = Parse("nodeType:Markdown namespace:acme"); + + parsed.IsContent.Should().NotBe(true); + parsed.Context.Should().BeNull( + "every internal query that names no context keeps its existing behaviour"); + } + + [Fact] + public void Is_main_still_parses_on_its_own() + { + var parsed = Parse("is:main"); + parsed.IsMain.Should().BeTrue(); + parsed.IsContent.Should().NotBe(true); + } + + [Fact] + public void HideFrom_is_additive_and_idempotent_so_two_declarers_cannot_drop_each_other() + { + var node = new MeshNode("Thing").HideFrom(MeshContexts.Search); + + // A module marks it, then a deployment's own config marks it again for something else. + // Neither may silently discard the other's. + var both = node.HideFrom(MeshContexts.Content).HideFrom(MeshContexts.Search); + + both.ExcludeFromContext!.Should().HaveCount(2); + both.ExcludeFromContext.Should().Contain(MeshContexts.Search); + both.ExcludeFromContext.Should().Contain(MeshContexts.Content); + } + + [Fact] + public void HideEverywhere_covers_the_three_a_person_can_reach() + { + var hidden = new MeshNode("Secret").HideEverywhere().ExcludeFromContext!; + + hidden.Should().HaveCount(3); + hidden.Should().Contain(MeshContexts.Search); + hidden.Should().Contain(MeshContexts.Create); + hidden.Should().Contain(MeshContexts.Content); + } + + [Fact] + public void HideFrom_with_nothing_to_add_leaves_the_node_alone() + { + var node = new MeshNode("Thing"); + node.HideFrom().ExcludeFromContext.Should().BeNull(); + } +} From a0723c88175190769d2b7a9336e0e03ecc2373ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roland=20B=C3=BCrgi?= Date: Mon, 24 Aug 2026 13:16:53 +0200 Subject: [PATCH 2/3] Spaces is what a shared workspace IS from the viewer's side MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 'let's just put Spaces for shared with me' — the tab answers 'what can I reach that I don't own', and Spaces names that better than describing how it got there. The key already existed in all four catalogs. Pinned and Spaces stay conditional (no pins / no grants ⇒ no tab); Mine is the viewer's own home path and is always there. Co-Authored-By: Claude Opus 5 (1M context) --- src/MeshWeaver.Graph/UserActivityLayoutAreas.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/MeshWeaver.Graph/UserActivityLayoutAreas.cs b/src/MeshWeaver.Graph/UserActivityLayoutAreas.cs index b524fe5a77..3da535a07d 100644 --- a/src/MeshWeaver.Graph/UserActivityLayoutAreas.cs +++ b/src/MeshWeaver.Graph/UserActivityLayoutAreas.cs @@ -859,12 +859,12 @@ internal static MeshSearchControl BuildContentSection( scopes.Add(ContentScope("home.mine", locale, cfg.DefaultSort, (_, suffix) => $"namespace:{nodeOwnerId} is:main is:content{SpacesDedupExclusions} {suffix}")); - // Shared — the cross-partition invitations (#385), which All folds in as a union leg. As a - // TAB it answers the different question ("what did someone give me?"), so it is worth its - // own place; but only when there is something in it, since the query is a path list and an - // empty list matches nothing at all. + // Spaces — the cross-partition invitations (#385), which All folds in as a union leg. As a + // TAB it answers a different question, and "Spaces" is what those things ARE from the + // viewer's side: the workspaces they can reach but do not own. Present only when there is + // something in it, since the query is a path list and an empty list matches nothing at all. if (sharedTargets is { Count: > 0 }) - scopes.Add(ContentScope("home.sharedWithMe", locale, HomeCatalogSort.LastModified, + scopes.Add(ContentScope("home.spaces", locale, HomeCatalogSort.LastModified, (_, suffix) => $"path:{string.Join("|", sharedTargets)} is:main is:content -nodeType:User{SpacesDedupExclusions} {suffix}")); From 9d4d090febd358e757dd231b04db5547476af249 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roland=20B=C3=BCrgi?= Date: Mon, 24 Aug 2026 13:30:55 +0200 Subject: [PATCH 3/3] Fix the doc example so it compiles if copied MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit HideFromContent() takes no arguments — the example showed it taking node names, which is exactly the kind of snippet someone pastes and then has to debug. It now shows the real shape: one call, on the node. The other review point on this PR is wrong: MeshWeaver.Hosting.Test does have the .Should() assertions (global usings), and the file builds clean under -c Release -warnaserror with all 7 of its tests green. No change there. Co-Authored-By: Claude Opus 5 (1M context) --- src/MeshWeaver.Mesh.Contract/MeshNodeVisibilityExtensions.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/MeshWeaver.Mesh.Contract/MeshNodeVisibilityExtensions.cs b/src/MeshWeaver.Mesh.Contract/MeshNodeVisibilityExtensions.cs index 2cb7a9e958..820b2921e2 100644 --- a/src/MeshWeaver.Mesh.Contract/MeshNodeVisibilityExtensions.cs +++ b/src/MeshWeaver.Mesh.Contract/MeshNodeVisibilityExtensions.cs @@ -13,8 +13,9 @@ namespace MeshWeaver.Mesh; /// .AddMeshNodes(new MeshNode("PluginRegistryCredential") { Name = "…" } /// .HideFrom(MeshContexts.Search, MeshContexts.Create, MeshContexts.Content)) /// -/// // In a deployment's final config (Systemorph/Memex), for something shipped by someone else: -/// .HideFromContent("Hosting", "Store/Plugin") +/// // In a deployment's final config (Systemorph/Memex), for something shipped by someone else — +/// // one call per node, on the node: +/// .AddMeshNodes(new MeshNode("Store/Plugin") { Name = "Plugin" }.HideFromContent()) /// /// /// On a NodeType DEFINITION node the mark covers every instance of that type — one line hides