
Security News
Happy Birthday, Shai-Hulud
It has been one year since Shai-Hulud made its first appearance on npm.
Zphil.LoadBearing.Cli
Advanced tools
One architecture spec, two render targets. LoadBearing checks a .NET solution against a fluent C# architecture spec — every rule carries a posture: Enforce for the law, Migrate for ratcheted debt with a grandfathered baseline, Quarantine for code contained rather than touched — and generates AI-agent context (a managed AGENTS.md block, MCP tools via 'loadbearing mcp') so the rules agents read are provably the rules the build enforces. Built for big, long-lived, business-critical .NET codebases.
LoadBearing is a .NET tool that renders one C# architecture spec to two targets: enforcement and agent context.
The architecture of a long-lived codebase is real: layers, boundaries, rules. It is also unenforced: it lives in a few heads, no build step checks it, and diagrams drift. Nothing fails when a change crosses a boundary, least of all when a coding agent wrote the change: fast, plausible, and blind to which walls are load-bearing. Architecture-as-code is LoadBearing's answer: the rules become one C# spec, and the spec becomes every surface on this page.
AGENTS.md block, per-directory rule cards, and MCP query tools for coding agents.Write your architecture once. Use it everywhere.
A rule is one statement:
arch.Rule("layering/domain-independent")
.Enforce(domain.MustNotReference(application, infrastructure, api))
.Because("The Domain holds the quote and rate model the rest of the subsystem is built on; it stays free of the layers that depend on it so it can be reasoned about and tested on its own.")
.Fix("Move the dependency out of Domain: define an interface here and implement it in the outer layer that needs it.");
That is the whole rule: an ID, a posture (Enforce), a constraint, a reason, a fix. It is committed in the clean-architecture example, and CI holds check green against the codebase it governs.
LoadBearing is pre-alpha and under construction; Status holds the current inventory.
Each target below consumes the same reified model, and every violation report carries the rule ID, the generated rule sentence, the reason, the fix, and the exact file:line.
| Target | What it is |
|---|---|
loadbearing check | one pass-or-fail verdict for the command line and CI |
check --sarif | that verdict as SARIF 2.1.0, for code scanning |
| xUnit adapter | every rule an individually named test |
loadbearing render | the managed AGENTS.md block and per-directory rule cards |
loadbearing mcp | arch_check, arch_status, arch_explain, arch_context, and arch_graph, plus a derive_spec prompt |
| agent hook | check when a turn ends; a red rule refuses the stop, report on stderr |
The adapter's failure text is byte-identical to the CLI's: the two share one renderer, and a product test pins them equal. The managed block plus loadbearing explain are also the generated architecture documentation, written for agents first and readable by people; the gate under The prose it generates keeps it current.
The compiler is the source of truth for your code. LoadBearing is the source of truth for your architecture.
LoadBearing governs itself. Thirty-five rules over this repository's real code, across eight declared layers, live in LoadBearingArchSpec.cs, and every fence from here down to This page is tested is that spec, or this solution under it, on one surface after another — bar the two under As SARIF, which come from the Meridian example, because showing a ratchet needs live debt and this repository has paid its own off. Take the rule that keeps the CLI off stdout — host is the layer the CLI project defines:
arch.Rule("cli/no-stdout")
.Enforce(host
.MustNotUse(
arch.Member(() => Console.Out),
arch.Member(typeof(Console), nameof(Console.Write)),
arch.Member(() => Console.WriteLine())))
.Because("Stdout is a protocol channel here — the MCP server speaks JSON-RPC over it and CLI " +
"output flows through System.CommandLine's console — so a direct Console write corrupts " +
"the wire and is invisible to the in-process tests.")
.Citation("https://modelcontextprotocol.io/specification/2025-06-18/basic/transports#stdio")
.Fix("Write CLI output through the command's InvocationConfiguration console; route server " +
"diagnostics to the logger or Console.Error.");
Nothing in the build system stops the CLI writing to Console, and the MCP server on the other side of that stdout speaks JSON-RPC over it. This rule is the only thing standing between those two facts.
loadbearing render derives the rule sentence from the constraint, carries the Because across verbatim, and writes the result into the managed block of this repository's committed AGENTS.md, the convention file Claude Code, Codex, Cursor, and Copilot read:
- `cli/no-stdout` — The Host layer must not use `Console.Out`, `Console.Write()` or `Console.WriteLine()`. Stdout is a protocol channel here — the MCP server speaks JSON-RPC over it and CLI output flows through System.CommandLine's console — so a direct Console write corrupts the wire and is invisible to the in-process tests. See <https://modelcontextprotocol.io/specification/2025-06-18/basic/transports#stdio>.
Nobody wrote that sentence, and nobody can let it go stale: SelfSpecTests.AgentsMd_IsCurrent composes the block in process and asserts the committed file already equals it. Its sibling ScopedCards_AreCurrent holds the whole class the same way, every per-directory card this repository commits, and also fails on a card that no rule placement produced, so one orphaned by a spec change cannot stay behind being read. The prose an agent reads is provably the spec the build enforces. Agents that query rather than read get the same model over MCP (loadbearing mcp).
The block is as current as the CLI that renders it. Nothing inside it records a version, so an older loadbearing re-renders it to that older tool's content without warning. Keep the tool and the spec's package reference in lockstep (see Installing), and re-render in CI to fail on a diff, the way this repository does for its examples.
Suppose an agent adds a progress printer to the CLI so a slow solution load stops looking hung, and reaches for Console.WriteLine. The agent finishes and tries to hand the work back. The Stop hook in hooks/ runs check over the working tree, the rule goes red, and the wrapper exits 2, which is how a Claude Code hook refuses a stop, with the report on the agent's stderr:
FAIL cli/no-stdout — The Host layer must not use `Console.Out`, `Console.Write()` or `Console.WriteLine()`.
because: Stdout is a protocol channel here — the MCP server speaks JSON-RPC over it and CLI output flows through System.CommandLine's console — so a direct Console write corrupts the wire and is invisible to the in-process tests.
citation: https://modelcontextprotocol.io/specification/2025-06-18/basic/transports#stdio
fix: Write CLI output through the command's InvocationConfiguration console; route server diagnostics to the logger or Console.Error.
subject: 154 types, 1 generated
src/Zphil.LoadBearing.Cli/Rendering/ProgressPrinter.cs:10 — Zphil.LoadBearing.Cli.Rendering.ProgressPrinter uses System.Console.WriteLine()
src/Zphil.LoadBearing.Cli/Rendering/ProgressPrinter.cs:15 — Zphil.LoadBearing.Cli.Rendering.ProgressPrinter uses System.Console.WriteLine()
That stanza is one rule's worth of the board the wrapper hands back whole. It carries the five things an agent needs to act without asking a human: the rule ID, the reason, the page that reason rests on, the fix, and the exact file:line of every offending write. The subject: line is scope rather than a finding, and appears only when a generator wrote some of what the rule swept. The agent routes the output through the command's console instead, and the next stop is clean. The correction lands in the turn that made the mistake, which is the last moment it costs nobody else anything.
The same spec runs inside a test project, where a team already looks. ArchRuleTests<TSpec> from the xUnit adapter turns each rule into an individually named test, and this repository's whole adapter dogfood is one class declaration:
[Collection("Serial")]
public sealed class AdapterSelfSpecTests : ArchRuleTests<LoadBearingArchSpec>
{
protected override string SolutionPath => FindSolutionUp("Zphil.LoadBearing.slnx");
}
Each test's display name is its rule ID, so a broken rule is named in the run summary rather than buried in an assertion message, and a Migrate rule's grandfathered sites keep their test green while the ratchet holds. CI runs it as a step of its own, "Self-spec as named xUnit tests (one test per rule)", whose log carries one line per rule ID.
check --sarif writes the same verdict as SARIF 2.1.0, which is what GitHub code scanning reads. A Migrate rule is where that matters most, and this repository no longer has one: its own ratchet reached zero and was promoted, which is the last step of the recipe rather than a gap in it. So the two fences below are the Meridian example, a mid-migration monolith retiring inline SQL out of its controllers over a counted baseline:
arch.Rule("data-access/no-inline-sql")
.Migrate(
from: "Controllers open SqlConnection and run inline SQL directly.",
to: arch.Namespace("Meridian.Web.Controllers.*")
.MustNotReference(typeof(SqlConnection), typeof(SqlCommand)))
.Because("Data access behind a repository can be tested and swapped; SQL in the request path cannot.")
.Fix("Move the SQL into a repository; see BookingRepository.");
Its twelve baselined sites keep the rule green at the command line and still reach code scanning, as note-level alerts, so the burndown is visible to anyone reviewing without ever failing a build. Code scanning reads a result's level and message and ignores both the suppression and the baseline state, so each site repeats its baseline attribution in the message, which is the line the alert shows. One result object from a fresh run:
{
"ruleId": "data-access/no-inline-sql",
"level": "note",
"message": {
"text": "Meridian.Web.Controllers.CustomsController references Microsoft.Data.SqlClient.SqlCommand (grandfathered in arch/baselines/data-access/no-inline-sql.json)"
},
"locations": [
{
"physicalLocation": {
"artifactLocation": {
"uri": "src/Meridian.Web/Controllers/CustomsController.cs",
"uriBaseId": "SRCROOT"
},
"region": {
"startLine": 27
}
}
}
],
"partialFingerprints": {
"loadBearingViolationIdentity/v1": "v1|T:Meridian.Web.Controllers.CustomsController|T:Microsoft.Data.SqlClient.SqlCommand||src/Meridian.Web/Controllers/CustomsController.cs|0"
},
"baselineState": "unchanged",
"suppressions": [
{
"kind": "external",
"justification": "grandfathered in arch/baselines/data-access/no-inline-sql.json"
}
]
}
A rule that is genuinely red lands the same shape at error level, with "baselineState": "new" and no suppressions array. In code scanning a reviewer tells house debt from a fresh breach by severity; a consumer that honours baselineState or suppressions can use either. Reproduce the file from a checkout:
dotnet build examples/Meridian/Meridian.slnx
loadbearing check examples/Meridian/Meridian.slnx --sarif loadbearing.sarif
CI's self-check job runs the same check over this repository's own spec on every push and uploads the SARIF it writes.
The same rule from check --json, the document arch_check returns over MCP, with the ratchet as counters rather than suppressions:
{
"id": "data-access/no-inline-sql",
"posture": "migrate",
"status": "passed",
"sentence": "Types in `Meridian.Web.Controllers.*` must not reference `SqlConnection` or `SqlCommand`.",
"because": "Data access behind a repository can be tested and swapped; SQL in the request path cannot.",
"fix": "Move the SQL into a repository; see BookingRepository.",
"baseline": {
"path": "arch/baselines/data-access/no-inline-sql.json",
"grandfathered": 12,
"stale": 0
},
"violationCount": 0,
"violations": [],
"warnings": []
}
Both counters go to zero when the last controller moves its SQL behind a repository, and the rule is then narrowed to what it actually meant, promoted to Enforce, and its baseline file deleted. That is what happened to this repository's own ratchet: the MCP infrastructure had migrated its environment reads to the IEnvironment seam, leaving two baselined sites the seam could not take, so the law was narrowed to the environment-variable members it was always about and the file went with it.
loadbearing graph surveys the codebase a spec is written against: projects and their references, namespaces and their sizes, every external dependency by root. Five of the twenty project lines for this solution:
Zphil.LoadBearing — 192 types; references: (none)
Zphil.LoadBearing.ArchSpec — 1 type; references: Zphil.LoadBearing, Zphil.LoadBearing.Packs.DotNet, Zphil.LoadBearing.Roslyn
Zphil.LoadBearing.Cli — 120 types (1 generated); references: Zphil.LoadBearing, Zphil.LoadBearing.Roslyn
Zphil.LoadBearing.Roslyn — 78 types (8 generated); references: Zphil.LoadBearing
Zphil.LoadBearing.Xunit — 2 types; references: Zphil.LoadBearing, Zphil.LoadBearing.Roslyn
The references: (none) on the first line is layering/core-no-roslyn seen from the other side: the rule forbids the reified model from reaching for the Roslyn project or the compiler packages behind it, and the survey shows it reaching for no other project in the solution. The (8 generated) is the count you check before writing a rule on a project: arch.Project names everything the assembly declares, so on a web tier where compiled views outnumber the code around them, that number is the difference between a law people can follow and one aimed at nobody. The lines not shown here are the test project, the rule pack, and the fixture projects the tests check against.
render --diagram <path> draws that same survey as a Mermaid diagram inside a committed file's managed block. Pointed at this repository and scoped to its six shipping projects, it writes ARCHITECTURE.md:
flowchart LR
accTitle: Codebase survey: Zphil.LoadBearing.slnx
accDescr: Projects in this solution and their cross-project references.
p_Zphil_LoadBearing["Zphil.LoadBearing"]
p_Zphil_LoadBearing_ArchSpec["Zphil.LoadBearing.ArchSpec"]
p_Zphil_LoadBearing_Cli["Zphil.LoadBearing.Cli"]
p_Zphil_LoadBearing_Packs_DotNet["Zphil.LoadBearing.Packs.DotNet"]
p_Zphil_LoadBearing_Roslyn["Zphil.LoadBearing.Roslyn"]
p_Zphil_LoadBearing_Xunit["Zphil.LoadBearing.Xunit"]
p_Zphil_LoadBearing_ArchSpec --> p_Zphil_LoadBearing
p_Zphil_LoadBearing_ArchSpec --> p_Zphil_LoadBearing_Packs_DotNet
p_Zphil_LoadBearing_ArchSpec --> p_Zphil_LoadBearing_Roslyn
p_Zphil_LoadBearing_Cli --> p_Zphil_LoadBearing
p_Zphil_LoadBearing_Cli --> p_Zphil_LoadBearing_Roslyn
p_Zphil_LoadBearing_Packs_DotNet --> p_Zphil_LoadBearing
p_Zphil_LoadBearing_Roslyn --> p_Zphil_LoadBearing
p_Zphil_LoadBearing_Xunit --> p_Zphil_LoadBearing
p_Zphil_LoadBearing_Xunit --> p_Zphil_LoadBearing_Roslyn
A solid arrow is a reference some type actually makes; a dotted arrow is a project reference that is declared and never exercised, which the text survey leaves you to work out by reading two of its sections against each other. There are no dotted arrows above, which is itself the report: no project here declares a reference it never uses. Nobody drew that diagram, and nobody can let it rot: SelfSpecTests.ArchitectureMd_IsCurrent composes the block in process and asserts the committed file already equals it. A hand-drawn architecture diagram is the artifact that rots first; this one is held to the code the same way the rules are.
That fence is drawn from what the code does. The same block carries a second one, drawn from what the spec forbids:
flowchart LR
accTitle: Architecture law: Zphil.LoadBearing.ArchSpec
accDescr: The places this spec names, the references it forbids, and the debt it grandfathers.
subgraph s_Core["Core"]
s_Model["Model"]
s_Checking["Checking"]
s_Rendering["Rendering"]
end
subgraph s_Extraction["Extraction"]
subgraph s_Zphil_LoadBearing_Roslyn_MsBuild["Quarantine: roslyn/msbuild-bootstrap"]
s_MsBuildBootstrap[["MsBuildBootstrap"]]
end
end
s_Microsoft_CodeAnalysis("Microsoft.CodeAnalysis.*")
s_Microsoft_Build("Microsoft.Build.*")
s_Pack["Pack"]
s_Adapter["Adapter"]
s_Core --x s_Extraction
s_Core --x s_Microsoft_CodeAnalysis
s_Core --x s_Microsoft_Build
s_Model --x s_Checking
s_Model --x s_Rendering
s_Pack -->|"only"| s_Core
s_Core --x s_Adapter
s_Extraction --x s_Adapter
s_Extraction --x|"expose"| s_Microsoft_Build
subgraph l_legend["Legend"]
l_ban["--x = must not reference"]
l_expose["--x expose = must not expose on a public signature"]
l_only["--> only = the only references allowed"]
l_quarantine["Quarantine box = a contained scope; the doubled boxes are its sanctioned surface"]
l_outside["Rounded box = a place named only as the target of a rule"]
l_nesting["A box inside a box = the inner place is part of the outer"]
end
Nothing in that drawing is a shape somebody chose for it. A bare --x is a reference this spec forbids, the labelled arrows are the verbs that need naming, and the box holding a doubled node is the quarantined scope with its sanctioned surface. A Migrate rule would draw a dotted arrow and add a legend row for it; this spec has none left, so neither is there, which is the legend earning its keep rather than a construct going missing. Model, Checking and Rendering sit inside Core because the spec defines each of them as a namespace cone inside the Core layer, so that nesting is declared rather than inferred. The legend is generated with the rest, one row per construct this particular drawing uses.
The line under the fence is the honest part. A diagram can only draw a rule whose subject and targets are places, and most of this spec's rules are about shapes, names, attributes and members instead. Those rules are listed by ID rather than quietly dropped, so the picture is never mistaken for the whole law.
The excerpts above are under gate. QuoteSyncTests holds each quoted excerpt to the committed file it was cut from, every line in order as a verbatim substring: change the spec and leave this page alone, and the suite goes red. ReadmeAnchorGateTests resolves the file:line anchors inside the quoted reports against the sources they name. The five fences that are captured tool output with no committed counterpart, the hook report and the SARIF object and the check document and the graph survey and the Framework check, are registered as such and held to their place on the page, so an exemption cannot quietly go dead.
The page is the tool's output, and the CI badge at the top is what keeps it that way.
NetArchTest and ArchUnitNET run architecture rules inside your unit tests, and they are good at it. LoadBearing moves the rules out of test code into one spec (architecture-as-code rather than architecture tests) and renders every surface above from it.
| Tool | What you write | Where it runs |
|---|---|---|
| NetArchTest | fluent assertions in test methods | your test runner |
| ArchUnitNET | ArchUnit-style rules in test classes | your test runner |
| LoadBearing | one spec in its own project | every target above |
The grammar comes from surveying that prior art, and GRAMMAR.md records each divergence. Constraints negate in the verb (MustNotReference), following ArchUnitNET. If you know ArchUnit's FreezingArchRule: what freezing does (accept a rule's current violations as a baseline) is Migrate with its counted baseline here. Quarantine contains a scope; it does not accept the scope's violations.
Because is mandatory. A rule without one is an invalid spec: check refuses to run it and reports every spec error in one pass. Even the predicate escape hatch, Must(condition, description:), does not compile without its description. Every reason ships to your agents in the rendered context, and in the Interchange example each of the twelve rules carries a Citation naming the learn.microsoft.com page its reason rests on. Nine of those twelve come from a shared rule pack, which is an ordinary class library of static methods: the pack owns the citation, the spec picks the posture. That pack ships in this repository as a working example rather than as a package to install, because a pack is a pattern you own rather than a registry you depend on.
Every rule carries one of the first two, and every scope one of the last two.
| Posture | What it is | What fails |
|---|---|---|
Enforce | the law | every violation, even ones predating the rule |
Migrate | a ratchet over a counted baseline | new violations; baselined sites stay quiet |
Quarantine | containment for a scope | a new reference into the scope |
Caution | dragons for code new callers are welcome to | nothing; a change set touching it draws a warning |
Enforce failing violations that predate it is what Migrate exists for: loadbearing baseline records a rule's current violations, new ones fail from the next commit, and the baseline only shrinks. At zero, the tool suggests promoting the rule to Enforce. Every scope carries a diff-aware tripwire: with check --diff-base <ref>, a change set that touches the scope draws a warning. For a Caution scope that tripwire is the whole posture: the dragons prose lands on the scope's directory as a card, explain and arch_context serve it, and no reference into the scope is ever a violation.
LoadBearing is built for long-lived, business-critical .NET codebases: systems too important to rewrite, with an architecture that is real but written down nowhere. Start where the code is:
Migrate.loadbearing baseline records every current violation on a counted, committed baseline.Enforce.The first draft is usually an agent's work, not a blank file. loadbearing mcp ships one prompt, derive_spec, the recipe that walks an agent from a solution with no spec to a reviewed proposal: survey the estate with arch_graph, scaffold the spec project, draft every hypothesis as a rule, and let check count the violations that assign each rule its posture. The tool does not infer the architecture and the agent does not ratify it: every proposed rule crosses a curation gate where you accept, edit, or drop it, and recording the baseline stays a human command. The Meridian adoption walkthrough is that recipe replayed on a committed example codebase, one real command at a time.
The tool runs on .NET 10. The codebase it checks does not have to, and neither does the spec that governs it.
A spec project can target net48 and compile at that framework's default language level, C# 7.3. It references the same netstandard2.0 Zphil.LoadBearing package every other spec does, and the CLI loads the built DLL in an isolated load context. A typeof() anchor works from there while the anchored type's own closure stays inside netstandard2.0; past that line, including .NET Framework types with no counterpart on .NET, a namespace pattern is the anchor, and it needs no assembly load at all.
Old project files load too. A non-SDK-style Framework project, the kind in the 2003 MSBuild XML namespace with explicit <Reference> items and a hand-maintained AssemblyInfo.cs, loads through the .NET Framework build host Roslyn ships and reports at file:line like anything else:
FAIL data-access/no-inline-sql — Types in `Classic.*` must not reference types in `System.Data.*`.
Classic.Billing/BillingCalculator.cs:10 — Classic.Billing.BillingCalculator references System.Data.SqlClient.SqlConnection
And the build server can stay where it is. check --binlog replays a binary log from a real build, including one produced by .NET Framework MSBuild.exe, so the machine that builds needs no .NET 10; only the machine that analyses does. Replaying that log and opening the workspace directly produce byte-identical output, which is what makes the replay a shortcut rather than a lesser reading.
The last two both need Windows, because that is where the Framework build host and MSBuild.exe come from. What the tool looks for is a vswhere-discoverable Visual Studio or Build Tools install carrying MSBuild\Current\Bin\MSBuild.exe; where several are installed it prefers VS 2019 or 2022, and when it has to take something outside that pair it names what it took, on stderr, beside any load failure. Set LOADBEARING_VS_INSTALL_PATH to an install root — the parent of MSBuild\Current\Bin — to choose one yourself. A net48 spec project carries no such requirement and builds anywhere.
Six worked examples in examples/ share one fictional freight-forwarding company. Four are solutions: CI builds each one, holds check green against the committed tree, and re-renders every managed block under examples/ to prove a zero diff. The other two walk a flow with captured output. Three are whole codebases:
Three go deeper on one surface each:
The CLI ships as a .NET global tool. The checker never builds the code it checks, so restore and build the solution first; a stale build gives stale verdicts:
dotnet tool install -g Zphil.LoadBearing.Cli
dotnet build MyApp.sln
loadbearing check MyApp.sln
The machine running it needs a .NET 10 SDK: commands that load a solution (check, render, status, graph, baseline, mcp) do so through MSBuildWorkspace via MSBuildLocator, and a runtime-only environment cannot host that load. The codebase under check has no version requirement of its own: LoadBearing never builds or retargets it, and it can target .NET Framework 4.8 or anything newer. The spec project compiles against one package, Zphil.LoadBearing, which is netstandard2.0.
A project that fails to load is treated as a wrong model rather than a smaller one: check, baseline, render, graph and status all exit 2 and say which projects failed, and --allow-workspace-diagnostics opts into the partial model. A project whose NuGet packages did not resolve is the same wrong model reached more quietly — it loads completely, and only the edges its package references would have produced are missing, so a rule about a package reports itself inert and the run goes green. That covers a restore that ran and failed and one that never ran at all: an SDK-style project writes obj/project.assets.json every time it restores, so not having one is a fact about that project, while a non-SDK-style .NET Framework project never writes one and is left alone. It gates on the same terms and takes the same opt-out, in its own words: the refusal names those projects separately and sends you to dotnet restore rather than dotnet build, because a project that loaded and a project that never did ask for different repairs. The xUnit adapter answers the same way in test dress: one named test fails carrying both causes, every rule case skips, and an AllowWorkspaceDiagnostics override opts in.
A solution filter is the opposite case: a smaller model rather than a wrong one. check, status, graph and the arch_context tool still answer, stamping which declared projects the filtered run never checked, and the JSON documents carry them as uncheckedProjects; a green over a subset can no longer read as a green over the solution. The verbs that read absence as evidence refuse instead: baseline --init, baseline --accept-reductions and render exit 2 rather than write files that vouch for projects the run never measured, and the fix they name is to run against the solution the filter references. The narrowing is measured from what actually loaded, not from the filter's text: a selection whose references pull in the whole solution narrows nothing and prints nothing. The xUnit adapter keeps every rule verdict and skips Workspace_LoadedCompletely, naming what was not checked. Committed baselines, render targets, arch_context paths, diff resolution and the reported project paths all resolve against the solution the filter references, never against the filter's own directory: a filter kept apart from its solution answers with the same paths as the solution itself. A rule whose whole selection lives in the unchecked projects reports as skipped, naming the filter, rather than failed for matching nothing; the fail-on-empty defaults stand untouched for unfiltered runs.
The command is loadbearing. Four lockstep-versioned packages make up a release:
| Package | What it is |
|---|---|
Zphil.LoadBearing.Cli | The loadbearing global tool: check, render, explain, status, graph, baseline, and the MCP server (loadbearing mcp). |
Zphil.LoadBearing | The spec contract, zero dependencies; the one package a spec project references. Reference the version loadbearing --version prints. |
Zphil.LoadBearing.Xunit | The xUnit adapter: every rule as an individually named test. |
Zphil.LoadBearing.Roslyn | Extraction/workspace infrastructure; a dependency of the above, not for direct reference. |
An MCP client launches the same tool with the mcp verb; the server speaks stdio. The
.mcp.json shape:
{
"mcpServers": {
"loadbearing": {
"command": "loadbearing",
"args": ["mcp", "MyApp.sln"]
}
}
}
For Claude Code, one command writes that same entry into the project's .mcp.json:
claude mcp add --scope project loadbearing -- loadbearing mcp MyApp.sln
The solution argument is optional, and most repositories should still pass it. Without it the
server reads LOADBEARING_SOLUTION_PATH, and when that is unset too it walks up from its
working directory to the first ancestor holding exactly one solution file, where a .slnf
filter counts only when no .sln or .slnx stands beside it. That resolves nothing where the
solution sits under src/, and refuses as ambiguous where several sit side by side, which
between them covers most real repositories. A solution file passed as the argument beats both.
When it cannot bind, the server starts anyway and every tool call returns the reason, naming
any solution it saw one level down. Each reply also names the CLI command that reads the same
model, because rebinding the server takes a client-config edit and a relaunch. The failure is
readable in the client rather than arriving as a server that would not start. However the
server is launched, the rule from Installing still applies: restore and build the solution
first; the checker never builds, and a stale build gives stale verdicts.
dnx launches the server straight from nuget.org without the global install; the -- hands
everything after it to the tool:
dotnet dnx Zphil.LoadBearing.Cli -- mcp MyApp.sln
This is how MCP-registry clients run the server; note the mcp subcommand. Spell it dotnet dnx
rather than bare dnx in a shell: on Windows the short form is a .cmd, which a POSIX shell will
not resolve. A config generated from the registry manifest passes no solution argument, so a
repository whose walk-up resolves nothing gets an unbound server. That session still works from
the CLI, but not through the installed command, because dnx runs the package without installing
anything: the server's replies name dotnet dnx Zphil.LoadBearing.Cli@<version> --yes -- in front
of the verb, pinned to the build answering. Bind such a repository at install time instead: put the
solution in the config's args after mcp, or set LOADBEARING_SOLUTION_PATH in its env.
dnx ships with the .NET 10 SDK, and one installed on the machine is the whole requirement.
dnx picks its own SDK from what is installed and never reads global.json, so a repository
pinning an older SDK launches the server anyway. Without any .NET 10 SDK there is no dnx
command, and a registry client sees a server that died before the handshake, with the reason
on stderr alone.
dotnet build Zphil.LoadBearing.slnx
dotnet test Zphil.LoadBearing.slnx
Pre-alpha, under construction. What this page shows is what exists: the reified model, the fluent builder, Roslyn extraction, the CLI verbs, the SARIF writer, the xUnit adapter, the MCP server, and the render pipeline, with all four postures evaluating. The spec excerpts above are quoted from this repository's own committed spec, the tool output is captured from runs against this solution, and CI uploads that check --sarif run to code scanning. The fluent surface can still move; GRAMMAR.md is its spec.
FAQs
One architecture spec, two render targets. LoadBearing checks a .NET solution against a fluent C# architecture spec — every rule carries a posture: Enforce for the law, Migrate for ratcheted debt with a grandfathered baseline, Quarantine for code contained rather than touched — and generates AI-agent context (a managed AGENTS.md block, MCP tools via 'loadbearing mcp') so the rules agents read are provably the rules the build enforces. Built for big, long-lived, business-critical .NET codebases.
We found that Zphil.LoadBearing.Cli demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.

Security News
It has been one year since Shai-Hulud made its first appearance on npm.

Research
/Security News
Operators behind PolinRider used a compromised GitHub account to plant malware in four development versions of a Packagist package with 700,000+ downloads.

Security News
GitHub Actions now supports cache-mode, a least-privilege control on the Actions cache aimed at the cache poisoning technique behind recent compromises.