Monorepo review has a scale problem that is not about size. A change that touches packages/client, services/auth, services/billing, and the CI config is not one change; it is four, with four owners and four risk profiles, and a reviewer scrolling one flat diff has to rebuild that structure in their head. Stacked-diff tools fix the problem at the commit level. PatchArc fixes it at the review level: every file in an Arc is classified into a scope, and the review reads as scopes.
What a scope is
A unit of ownership: a package, a service, a module. It has an id, a title, a kind, one or more path globs, optional owners, and optional dependencies. Scopes come from detection or from .patcharc/config.yaml; explicit config always wins.
Detection
patcharc scopes runs ten detectors and merges what they find:
| Layout | Source of truth |
|---|---|
| pnpm | pnpm-workspace.yaml packages list |
| npm / yarn | package.json workspaces (array or object form) |
| Nx | nx.json plus each project.json |
| Rush | rush.json projects |
| Bazel | every BUILD / BUILD.bazel |
| Go | go.work use entries |
| Cargo | [workspace] members |
| Maven | non-root pom.xml files |
| Gradle | settings.gradle(.kts) plus build files |
| .NET | every *.csproj |
A turbo.json is recognised but contributes nothing by itself; Turborepo workspaces are pnpm or npm workspaces underneath, which are detected. There is no Python detector in 0.2.0; declare Python packages explicitly.
Classification
For every changed file, in order: graph cross-cutting globs, then config cross_cutting globs, then each scope's paths in declaration order, else unclassified. Cross-cutting files (lockfiles, CI, shared tooling) get their own bucket so they do not pollute a service's summary. Unclassified files are listed under their own heading; nothing is silently dropped.
Declaring scopes and owners
scopes:
- id: auth
title: Auth service
kind: service
paths: ["services/auth/**"]
owners: ["@platform"]
depends_on: ["client"]
- id: client
title: Shared client
kind: package
paths: ["packages/client/**"]
cross_cutting:
- ".github/**"
- "pnpm-lock.yaml"
Ids must match [a-z0-9_-]+ and be unique. Globs support **, *, and ?. depends_on is the only source of edges for patcharc scopes:graph, which prints Graphviz DOT; detectors do not infer dependencies yet.
What the review shows
Per touched scope: the commits and files inside it (computed per commit with git diff-tree), additions and deletions, and a severity band: major at 500 or more changed lines, moderate at 100, minor below. A reviewer opens the capsule and sees "auth: major, 3 commits, 11 files" next to "client: minor, 1 commit, 2 files" and knows where to spend the hour.
Working alongside stacked diffs
If your team uses Graphite or Sapling, stacks already split the change by commit; PatchArc's scopes split it by ownership. They compose: each stacked PR can carry its own Arc, or one Arc can span the stack and the scope summaries show which layer touched which package. The observer handles rebases and amends because it reads plumbing, not refs.
Try it on your repository
$ patcharc scopes
$ patcharc scopes:graph | dot -Tsvg > scopes.svg
If detection gets it wrong, declare scopes in config; they win. The scopes guide has the full reference.