Architecture rules and plans
branchscope.json layers and rules, PLAN.md plans, and how reviews apply them.
Rules make a review flag what matters to your codebase. They live in branchscope.json at the repository root, next to the code they describe, and are versioned with it.
Layers and rules#
A layer is a named selection of graph nodes. A rule says which edges from one layer to another are forbidden (deny) or need a look (review).
{
"layers": {
"controllers": { "nodes": ["dotnet:src/Billing.Api|Billing.Api.Controllers*"] },
"data": { "nodes": ["dotnet:src/Billing.Api|Billing.Api.Data*"] },
"tables": { "kinds": ["table"] },
"personal": { "tags": ["pii"] }
},
"rules": [
{ "id": "controllers-no-data", "description": "Controllers go through services.", "from": "controllers", "to": "data", "edges": ["imports", "references"], "effect": "deny" },
{ "id": "table-writes", "to": "tables", "edges": ["writes"], "effect": "review" },
{ "id": "pii-reads", "to": "personal", "edges": ["reads"], "effect": "review" }
]
}
Layers:
- What a layer selects: by node-ID globs (
nodes, where*matches anything), bykinds, and bytags. A node must match every field given. - Finding node IDs: they're in the graph explorer, under each node's name.
Rules:
fromandtoname layers. Leave one out to mean "any node".edgeslimits the rule to some edge kinds.effectisdeny(a violation) orreview(flagged for a person).- Channels: a layer can select channels by kind (
"kinds": ["channel"]) or by ID ("nodes": ["msg:events:invoice-*"]). With such a layer,invoice-events, the rule{ "id": "web-no-invoice-events", "from": "web", "to": "invoice-events", "edges": ["publishes"], "effect": "deny" }leaves invoice events to the billing service.
What a change is responsible for:
- New: an edge that matches in the head but didn't in the base. This includes an old edge that starts matching because a node gained a tag or moved into a layer.
- Existing: matches in both. These are counted, not blamed, so you can add a rule to a codebase that already breaks it.
- Fixed: matched in the base, not in the head.
- Renamed and moved code: an edge that was only renamed or moved stays existing.
The target's rules apply: reviews use the rules of the target branch, so a change can't remove the rule it breaks. A change that edits the rules is noted, and its rules apply once it's merged.
Plans#
A change can say what it means to touch, in a branchscope block in PLAN.md at the repository root:
# Plan
Add invoice creation: an endpoint in the API, a client call in the web app.
```branchscope
{
"touches": { "files": ["src/Billing.Api/**", "apps/web/**"], "layers": ["controllers"] },
"mustNotTouch": { "files": ["branchscope.json"], "layers": ["data"] }
}
```
- Dimensions:
files(path globs:*within a segment,**across segments),nodes(ID globs) andlayers. - mustNotTouch: anything the change touches that this matches is shown as changes it rules out.
- touches: anything the change touches outside it is shown as outside what it says it touches. That's for information; plans drift honestly.
- What counts as touched:
- every changed file, both sides of a rename included;
- every added, removed or changed node, both IDs of a renamed one included;
- the calling side of every changed edge.
Why plans matter for agents: an agent can write its plan first, and the review shows whether the code it produced stayed inside it.
Other settings in branchscope.json#
| Key | What it's for |
|---|---|
services | Names HTTP services and where they're served from and reached at, so endpoints join across languages ({"billing-api": {"project": "src/Billing.Api", "baseUrls": ["http://localhost:5000"]}}) |
stores | Names databases by EF Core context, for table IDs |
httpClients | Your own HTTP wrapper functions, so calls through them become endpoint requests |
generated | Path globs of committed generated code, tagged generated |
brokers | Names message brokers, each with its system, optionally the projects that use it and channel names of configuration keys, so channels join across languages and repositories ({"events": {"system": "kafka", "names": {"env:INVOICE_TOPIC": "invoice-created"}}}) |
messageClients | Your own publish and subscribe wrapper functions (TypeScript), so calls through them become channel edges |
An invalid branchscope.json fails the graph build with a message, rather than guessing.