This file provides guidance to AI agents when working with code in this repository.
NEVER commit or push automatically. Always wait for the user to explicitly ask for a commit or push. Present changes for review first.
The gh CLI token has read + push permissions but cannot merge PRs, resolve review threads, or request reviewers. For these operations:
# 1. Find unresolved threads
gh api graphql -f query='
query($owner:String!, $repo:String!, $pr:Int!) {
repository(owner:$owner, name:$repo) {
pullRequest(number:$pr) {
reviewThreads(first:100) {
nodes { id isResolved }
}
}
}
}' -f owner=Systemorph -f repo=MeshWeaver -F pr=PR_NUMBER \
--jq '.data.repository.pullRequest.reviewThreads.nodes[] | select(.isResolved==false) | .id'
# 2. Resolve each thread
gh api graphql -f query='mutation($id:ID!){ resolveReviewThread(input:{threadId:$id}){ clientMutationId }}' -f id=THREAD_ID
# 3. Merge
gh pr merge PR_NUMBER --mergeIf these fail with FORBIDDEN, the token lacks write scope — do it from the GitHub UI or re-authenticate with ! gh auth login.
Documentation is embedded in src/MeshWeaver.Documentation/ and served under the Doc/ namespace at runtime.
The documentation on the architecture is accessible via src/MeshWeaver.Documentation/Data/Architecture/
Topics: Message-based communication, Actor model, UI streaming, AI agents, Data versioning, Serialization, Access control, Partitioned persistence, Business rules & calculations
The documentation on the data mesh is accessible via src/MeshWeaver.Documentation/Data/DataMesh/
Topics: Node type configuration, Query syntax, Unified Path references, Interactive markdown, Collaborative editing, CRUD operations, Data modeling
The documentation on the GUI is accessible via src/MeshWeaver.Documentation/Data/GUI/
Topics: Container controls (Stack, Tabs, Toolbar, Splitter), Layout grid, DataGrid, Editor, Observables, Data binding, Attributes, Reactive dialogs
The documentation on AI integration is accessible via src/MeshWeaver.Documentation/Data/AI/
Topics: Agentic AI, MCP authentication, MeshPlugin tools (Get, Search, Create, Update, Delete, NavigateTo)
The documentation on deployment is accessible via src/MeshWeaver.Documentation/Data/Architecture/Deployment.md
Topics: Aspire CLI deployment, deployment modes (local/test/prod/monolith), secrets management, Azure Container Apps, PostgreSQL, Orleans clustering, infrastructure provisioning
Quick deploy commands (run from repo root):
- Prod:
aspire deploy --project memex/aspire/Memex.AppHost/Memex.AppHost.csproj -- --mode prod - Test:
aspire deploy --project memex/aspire/Memex.AppHost/Memex.AppHost.csproj -- --mode test
Prerequisites: Azure CLI authenticated, Aspire CLI installed, Docker running. See the full deployment doc for details.
Built-in agent definitions are embedded in src/MeshWeaver.AI/Data/Agent/
Agents: Executor, Navigator, Planner, Research
Stay in the root directory (C:\dev\MeshWeaver) and use simple, single commands. Chained commands (&&, ||), for loops, and cd all require user confirmation — avoid them.
# CORRECT — simple single commands from root directory
dotnet build src/MeshWeaver.Graph/MeshWeaver.Graph.csproj
dotnet test test/MeshWeaver.Graph.Test --no-build
# WRONG — these all require extra approval:
cd /c/dev/MeshWeaver && dotnet build # chained cd
for d in test/*; do dotnet test $d; done # for loop
dotnet build && dotnet test # chained commands# Build entire solution
dotnet build
# Run tests (uses xUnit v3)
dotnet test
# Run specific test project (example)
dotnet test test/MeshWeaver.Data.Test/MeshWeaver.Data.Test.csproj
# Clean solution
dotnet clean
# Restore packages
dotnet restoredotnet run --project memex/Memex.Portal.Monolith
# Access at https://localhost:7122The Memex Portal uses AddGraph() to dynamically load Graph nodes from samples/Graph/Data/, and AddDocumentation() to serve embedded documentation under the Doc/ namespace. This is the recommended portal for development.
dotnet run --project memex/aspire/Memex.AppHost
# Access Aspire dashboard for service management
# Requires Docker for dependenciesRule: await inside hub handlers, button click actions, and service layers that are called from those paths is FORBIDDEN. It deadlocks. Every write/read to the mesh must be composed as an IObservable<T> chain.
This is the single most important pattern in MeshWeaver. Violating it is the cause of most "button does nothing", "popup doesn't show", and "freezes under load" bugs.
IMeshService.CreateNode / UpdateNode / DeleteNodereturnIObservable<T>(NOTTask<T>). They internallyhub.Post+hub.RegisterCallback. Subscribe to drive them — never call.ToTask()/.FirstAsync()/awaiton them from a click action or hub handler.- Click actions must be synchronous:
WithClickAction(ctx => { ...; return Task.CompletedTask; }). Neverasync ctx => await .... - Read form data via
Subscribe(...)withTake(1), notawait FirstAsync(). The data stream emits its current value synchronously on subscribe.
.WithClickAction(ctx =>
{
// Immediate optimistic UI feedback — the click registered.
ctx.Host.UpdateData(resultId, "<p>Working…</p>");
// Read form data via Subscribe (sync emission for BehaviorSubject-style streams).
ctx.Host.Stream.GetDataStream<Dictionary<string, object?>>(formId)
.Take(1)
.Subscribe(data =>
{
var label = data?.GetValueOrDefault("label")?.ToString() ?? "";
if (string.IsNullOrEmpty(label))
{
ctx.Host.UpdateData(resultId, "<p>Please enter a label.</p>");
return;
}
// Reactive service call — returns IObservable<T>, no await.
// Service internally composes meshService.CreateNode/UpdateNode/DeleteNode chains.
myService.DoWork(label).Subscribe(
result => ctx.Host.UpdateData(resultId, $"<p>Done: {result}</p>"),
ex => ctx.Host.UpdateData(resultId, $"<p>Error: {ex.Message}</p>"));
});
return Task.CompletedTask; // ← click action itself is sync
})Compose IObservable chains with SelectMany, Select, FirstOrDefaultAsync. Return IObservable<T> (not Task<T>) from any method that will be called from a hub handler or click action.
public IObservable<TokenCreationResult> CreateToken(...)
{
var userNode = new MeshNode(...);
return nodeFactory.CreateNode(userNode) // IObservable<MeshNode>
.SelectMany(created =>
{
var indexNode = new MeshNode(...) { ... };
return nodeFactory.CreateNode(indexNode) // chain the second write
.Select(_ => new TokenCreationResult(raw, created));
});
// No await anywhere. The consumer calls .Subscribe(onNext, onError).
}
// Wrap IAsyncEnumerable queries into observables:
public IObservable<bool> DeleteToken(string path) =>
Observable.FromAsync(() =>
meshQuery.QueryAsync<MeshNode>(MeshQueryRequest.FromQuery($"path:{path}"))
.FirstOrDefaultAsync().AsTask())
.SelectMany(node =>
{
/* ... */
return nodeFactory.DeleteNode(path); // IObservable<bool>
});// ❌ DEADLOCKS the hub under load.
.WithClickAction(async ctx =>
{
var data = await ctx.Host.Stream.GetDataStream<T>(id).FirstAsync();
var result = await myService.DoWorkAsync(data); // never awaiting hub-backed services
ctx.Host.UpdateData(resultId, result);
})
// ❌ Task.Run is a crutch, not a fix — identity doesn't flow, failures are invisible.
.WithClickAction(ctx =>
{
_ = Task.Run(async () => { await myService.DoWorkAsync(); });
return Task.CompletedTask;
})
// ❌ Hub handlers must NOT await mesh writes either.
public async Task<IMessageDelivery> HandleFoo(IMessageDelivery<FooRequest> req)
{
await meshService.CreateNodeAsync(...); // deadlock risk
return req.Processed();
}- Top-level app startup code (
Main,ConfigureServices,InitializeAsyncof test base classes). - Pure CPU / file-I/O work that does NOT flow through the hub (e.g.,
File.ReadAllTextAsync). - Test code that explicitly wants to block until a stream emits (use
.FirstAsync().ToTask()then await, but only in tests).
Everywhere else, the shape is Subscribe(onNext, onError). If a service you need only exposes …Async / Task<T>, add a reactive overload that returns IObservable<T> and refactor.
NEVER use mutable collections. Always use System.Collections.Immutable:
List<T>→ImmutableList<T>.Empty+= list.Add(item)Dictionary<K,V>→ImmutableDictionary<K,V>.Empty+= dict.SetItem(key, val)HashSet<T>→ImmutableHashSet<T>.Empty+= set.Add(item)Queue<T>→ImmutableQueue<T>.Empty+= queue.Enqueue(item)/= queue.Dequeue(out var item).ToList()→.ToImmutableList(),.ToHashSet()→.ToImmutableHashSet()
The codebase is distributed (Orleans, reactive streams). Mutable collections cause race conditions and unpredictable behavior. The only exception is ConcurrentDictionary for thread-safe concurrent mutation patterns.
Message Hub Architecture: MeshWeaver is built on an actor-model message hub system (MeshWeaver.Messaging.Hub). All application interactions flow through hierarchical message routing with address-based partitioning (e.g., @app/Address/AreaName).
Layout Areas: The UI system uses reactive Layout Areas - framework-agnostic UI abstractions that render in Blazor Server. Layout areas are addressed by route and automatically update via reactive streams.
AI-First Design: First-class AI integration using Microsoft.Extensions.AI with plugins (MeshPlugin, LayoutAreaPlugin) that provide agents access to application state and functionality.
-
src/- Core framework libraries (50+ projects)MeshWeaver.Messaging.Hub- Actor-based message routingMeshWeaver.Layout- Framework-agnostic UI abstractionsMeshWeaver.AI- Agent framework with plugin architectureMeshWeaver.Blazor- Blazor Server implementationMeshWeaver.Data- CRUD operations with activity trackingMeshWeaver.Documentation- Embedded documentation (served under Doc/)MeshWeaver.Graph- Graph node configuration and node type system
-
samples/- Sample business domain applicationsGraph/Data/- Sample data nodes (ACME, Northwind, Cornerstone, etc.)Graph/content/- Static content files (icons, images, attachments)
-
memex/- Memex Portal (recommended for development)Memex.Portal.Monolith/- Development portal with full Graph supportaspire/- Microservices with .NET Aspire orchestration
Request-Response: Use hub.AwaitResponse<TResponse>(request, o => o.WithTarget(address)) for operations requiring results.
The response is submitted as hub.Post(responseMessage, o => o.ResponseFor(request)).
Fire-and-Forget: Use hub.Post(message, o => o.WithTarget(address)) for notifications and events.
Address-Based Routing: Services register at specific addresses (e.g., bookings/q1_2025, app/northwind, pricing/id).
Layout areas follow the pattern @{address}/{areaName}/{areaId}. The areaId is optional and depends on the view.
E.g. {address}/Details/{itemId} would render a details view for the item with itemId.
Layout areas are typically kept on the same address as the underlying data.
Reactive UI: All UI state changes flow through the message hub. Controls are immutable records that specify their current state.
IMPORTANT: Application code must never use IMeshStorage or IMeshCatalog directly — these are internal infrastructure interfaces.
var query = hub.ServiceProvider.GetRequiredService<IMeshService>();
var node = await query.QueryAsync("path:org/Acme", maxResults: 1).FirstOrDefaultAsync(ct);var factory = hub.ServiceProvider.GetRequiredService<IMeshNodeFactory>();
await factory.CreateNodeAsync(node, createdBy: userId, ct);
await factory.DeleteNodeAsync(path, recursive: true, ct);hub.Post(new UpdateNodeRequest(updatedNode));
await hub.AwaitResponse(new MoveNodeRequest(sourcePath, targetPath), ct);
hub.Post(new DataChangeRequest { Updates = [entity] });Always use GetRequiredService<T>() for core services (IMeshNodeFactory, IMeshService). Never use GetService<T>() + null check for services that must be registered.
For full documentation see src/MeshWeaver.Documentation/Data/Architecture/DataAccessPatterns.md.
public static class MyLayoutArea
{
public static void AddMyLayoutArea(this LayoutConfiguration config) =>
config.AddLayoutArea(nameof(MyLayout), MyLayout);
public static UiControl MyLayout(LayoutAreaHost host, RenderingContext ctx) =>
Controls.Stack
.WithView(Controls.Html("Some text")
.WithView(Controls.Markdown("Some markdown view"))
);
}We support rich markdown with mermaid diagrams, code blocks, MathJax,
and live execution via dynamic markdown. Layout areas can be inserted by
using @{address}/{areaName}/{areaId}
Messages are registered in the configuration of the hub. Also DI is set up on the level of hub configuration:
public static class NorthwindHubConfiguration
{
public static MessageHubConfiguration AddNorthwindHub(this MessageHubConfiguration config)
{
return config.AddHandler<MyRequestAsync>(HandleMyRequestAsync)
.AddHandler<MyRequest>(HandleMyRequest);
}
public static async Task<IMessageDelivery> HandleMyRequestAsync(MessageHub hub, IMessageDelivery<MyRequestAsync> request, CancellationToken ct)
{
// Process the request
var result = await SomeService.ProcessAsync(request.Message);
// Send response
await hub.Post(new MyResponse(result), o => o.ResponseFor(request));
return request.Processed();
}
public static IMessageDelivery HandleMyRequest(MessageHub hub, IMessageDelivery<MyRequest> request)
{
// Process the request
var result = SomeService.Process(request.Input);
// Send response
hub.Post(new MyResponse(result), o => o.ResponseFor(request));
return request.Processed();
}
}public class MyPlugin(IMessageHub hub, IAgentChat chat)
{
[Description("Description on how to use")]
public async Task<string> DoSomething([Description("Description for input")]string input)
{
var request = new MyRequest(input); // Create a request object
var address = GetAddress(request); // Get the address for the plugin, e.g., "app/northwind"
// Use the message hub to send a request and receive a response
var response = await hub.AwaitResponse<MyResponse>(request, o => o.WithTarget(address));
return JsonSerializer.Serialize(response.Message, hub.JsonSerializationOptions);
}
public Address GetAddress(MyRequest request)
{
// Logic to determine the address based on the request
// the chat contains a context, which is usually good to use.
// can also contain agent specific mapping logic.
return chat.Context.Address;
}
}- .NET 10.0 - Target framework
- Orleans - Distributed deployment (distributed deployment, microservices)
- Blazor Server - Web UI framework
- Microsoft.Extensions.AI - AI integration
- xUnit v3 - Testing framework
- FluentAssertions - Test assertions
- Chart.js - Data visualization
- Azure SDKs - Cloud integration
- Markdig - Markdown processing
Tests use xUnit v3 with structured logging and test parallelization configured via xunit.runner.json:
parallelizeAssembly: falseparallelizeTestCollections: falsemaxParallelThreads: 1methodTimeout: 60000ms(1 minute per test method)
No mocking. Tests that need infrastructure (persistence, messaging, DI) must use MonolithMeshTestBase or OrleansTestBase — never mock IMessageHub, IMeshService, or other core interfaces.
For implementing and testing satellite entities (comments, threads, tracked changes), see src/MeshWeaver.Documentation/Data/Architecture/SatelliteEntityPatterns.md.
Key rules:
- Handler must be synchronous (
IMessageDelivery, notasync Task<IMessageDelivery>) - Use
meshService.CreateNode()(Observable) +.Subscribe(onNext, onError)— neverawait - Use
workspace.UpdateMeshNode()for parent node content updates (in-memory, persisted via debounce) - Post response inside the
Subscribe(onNext)callback, not before - Orleans tests: client configurator must call
AddGraph()for type registry alignment - Verify via
GetDataRequestorGetRemoteStream— neverQueryAsyncin distributed tests
Run tests from the root directory using sub-paths. Do NOT write output to /tmp or temp directories — test results (.trx) are automatically collected in the project's bin/ directory.
CRITICAL: Always use run_in_background: true for test runs. Tests can take minutes — never block the conversation waiting for them. Use timeout: 180000 (3 min) max for Bash test commands. The xunit.runner.json methodTimeout is 60000ms (1 min) per test method.
Do NOT use --verbosity minimal (or -v m) when tests are expected to fail. Minimal verbosity hides error details (stack traces, assertion messages), forcing you to re-run with normal verbosity — wasting time and frustrating the user. Use default verbosity or --verbosity normal so failures are visible on the first run. Only use --verbosity minimal when you are confident all tests will pass and just need a quick green/red check.
# Run from root directory with sub-path
dotnet test test/MeshWeaver.Hosting.Monolith.Test --no-restore
# Run a specific test project
dotnet test test/MeshWeaver.Graph.Test --no-restore
# Filter to specific tests
dotnet test test/MeshWeaver.Graph.Test --filter "ClassName~AccessAssignment" --no-restoreWorkflow:
- Run tests once in background (
run_in_background: true) - If failures: read the output to understand errors — do NOT re-run
- Fix the code
- Run tests once again to verify fixes
- Repeat 2–4 until green
MonolithMeshTestBase automatically logs in rbuergi@systemorph.com as Admin via TestUsers.DevLogin(Mesh) in InitializeAsync(). This means all tests start with a logged-in admin user — no manual setup needed for basic CRUD.
TestUsers (MeshWeaver.Hosting.Monolith.TestBase.TestUsers):
TestUsers.Admin— default admin AccessContextTestUsers.SampleUsers()— MeshNode array of sample users fromsamples/Graph/Data/User/TestUsers.DevLogin(mesh)— logs in the admin user (called automatically by base class)builder.AddSampleUsers()— extension to pre-seed user MeshNodes inConfigureMesh
When tests with AddRowLevelSecurity() need per-user access control (e.g., testing that User1 can't see User2's data), use explicit admin setup for data creation:
// Before creating test data: set up admin context
var accessService = Mesh.ServiceProvider.GetRequiredService<AccessService>();
var securityService = Mesh.ServiceProvider.GetRequiredService<ISecurityService>();
await securityService.AddUserRoleAsync("setup-admin", "Admin", null, "system");
accessService.SetCircuitContext(new AccessContext { ObjectId = "setup-admin", Name = "Setup Admin" });
// ... create test nodes ...
// After setup: clear admin context so tests start clean
accessService.SetCircuitContext(null);Only use registered node types in tests. Standard types registered by AddGraph():
Markdown, Code, Agent, Group, User, VUser, Role, Notification, Approval, AccessAssignment, GroupMembership, PartitionAccessPolicy, ActivityLog, UserActivity, Comment, Thread, ThreadMessage
Custom types can be registered via builder.AddMeshNodes(new MeshNode("MyType") { Name = "My Type" }) in ConfigureMesh.
Reference MeshWeaver.Hosting.Monolith.TestBase and inherit from MonolithMeshTestBase:
public class MyTest(ITestOutputHelper output) : MonolithMeshTestBase(output)
{
// Override ConfigureMesh to add services and sample users
protected override MeshBuilder ConfigureMesh(MeshBuilder builder)
=> base.ConfigureMesh(builder)
.AddGraph()
.AddSampleUsers()
.ConfigureHub(hub => hub.AddMyHub());
[Fact]
public async Task MyTestMethod()
{
var meshQuery = Mesh.ServiceProvider.GetRequiredService<IMeshService>();
var nodeFactory = Mesh.ServiceProvider.GetRequiredService<IMeshNodeFactory>();
// Create test data
await nodeFactory.CreateNodeAsync(new MeshNode("test", "Namespace") { Name = "Test" }, "testuser");
// Query
var result = await meshQuery.QueryAsync<MeshNode>("path:Namespace/test").FirstOrDefaultAsync();
result.Should().NotBeNull();
}
}public class MyTest : HubTestBase, IAsyncLifetime
{
protected override MessageHubConfiguration ConfigureHost(MessageHubConfiguration config)
=> base.ConfigureHost(config).AddNorthwindHub();
protected override MessageHubConfiguration ConfigureClient(MessageHubConfiguration config)
=> base.ConfigureClient(config).AddLayoutClient();
[Fact]
public async Task MyTestMethod()
{
var hub = GetClient();
var response = await hub.AwaitResponse<MyResponse>(request, o => o.WithTarget(new HostAddress()));
response.Should().NotBeNull();
}
}- Framework code belongs in
src/ - Test code belongs in
test/ - Sample applications go in
samples/ - Each module should have its own set of hubs and address spaces (e.g.,
@app/northwind) - UI components should be framework-agnostic in the layout layer. The language are the controls inheriting from
UiControl. - AI agents should use plugins to access application functionality
The solution uses centralized package management via Directory.Packages.props. When adding new dependencies, update the central package file rather than individual project files.
Directory.Build.props- Global MSBuild properties and versioningDirectory.Packages.props- Centralized NuGet package version managementnuget.config- NuGet package sources configurationxunit.runner.json- Test execution configuration
- Main branch:
main(use for PRs) - Solution file:
MeshWeaver.slnxcontains 50+ projects