Trust is earned, not given

A different perspective

2026-07-23 · Projects

AI-Enabled RMA, part 2: Eight projects, one dependency rule

Part 2from the AI-Enabled RMA series · 6 parts in all

AIEnabledRMA ships as one solution file, AIEnabledRma.slnx, with eight projects and a single dependency rule: domain code never references I/O, and every host depends on the same registrations. This article is the map — what each project is for, and why the boundaries are where they are.

The projects

The rule that matters most

The three hosts — web, MCP and the RAG service — call the same registration methods. AddRmaDbContext plus AddRmaData give every host the same repositories and, critically, the same fuzzy-match thresholds. AddRag gives them the same retriever over the same corpus. AddAiProvider gives them the same model switch.

The comment in the README is the actual design goal: the fuzzy-match thresholds a customer-facing wizard uses and the ones an agent's lookup tool uses cannot drift apart. A drift there would be uniquely nasty, because an agent would confidently tell a customer that a serial resolves to nothing while the wizard happily found it a second earlier.

Configuration as the extension point

Almost everything a deployment would want to change is configuration, not C#. The Policy section carries the return window and grace period, the repair price table, the warranty tiers, the excluded causes with their match modes, the fraud-indicator prefixes, the human-review-only regions, and the advisory-only lockdown switch. Adding a tier, a return window or an excluded cause never requires editing code — which is the property that keeps the policy testable as data rather than re-testing as a build.

Package versions are pinned centrally in Directory.Packages.props with ManagePackageVersionsCentrally, and the shared target framework is net10.0 from Directory.Build.props. The Microsoft.Extensions.* pins are held at the same servicing band as the EF Core packages, because a lower pin trips NU1109 downgrade detection — a small detail that saves an afternoon the first time it happens.

Getting it running in five minutes

docker compose up -d                                  # PostgreSQL 16 on :5432
export ConnectionStrings__Rma='Host=localhost;Port=5432;Database=rmadev;Username=rmauser;Password=rmapassword'
dotnet run --project tools/DbAdmin -- reset --force  # drop schema, migrate, seed
dotnet run --project src/AIEnabledRma.Web            # the wizard on http://localhost:5xxx

The checked-in connection string is a placeholder and nothing that touches the database works until a real one is supplied through the environment variable. The seeder gives you a device for every policy outcome — one in warranty, one expired but inside the window that walks the paid repair path for $69.00, one shipped 1,200 days ago that is refused, one with an open return that routes to human review, and one serial that does not exist. Being able to demo every branch of a decision engine in one screen is worth more than any amount of documentation.

Two containers ship as well, for the web app and the MCP server, so the deployable shape is not a configuration exercise left to the reader.

Why this layout, and not something cleverer

The alternative structure — one project, or a project per feature — was rejected for a specific reason. The whole point of this sample is that the policy layer is provably separate from the model. A folder boundary would not have demonstrated that; a project boundary with no dependency from Domain to Ai does, and it fails at compile time rather than at review time when someone tries to shortcut it.

Next: inside the evaluator — nine ordered rules, a rule trace, and how money is priced.

Repository: github.com/bobhuang1/AIEnabledRMA