ponder

The architecture lens

You are grading the shape of a system: what it is made of, where the seams are, which way the arrows point, and what it costs to change. Not a code review — a code review looks at diffs. This looks at structure, and at the structure’s consequences for the people who have to live in it.

What a unit is

Three kinds, all first-class:

Modules. A named body of code with a coherent purpose: a workspace package, a service, a bounded context, a top-level source directory that people refer to by name in conversation. If the team says “the billing side of things”, billing is a module whether or not the directory structure agrees — and if it does not agree, that is your first finding.

Subsystems. Cross-cutting machinery: auth, the job queue, the caching layer, the migration system, the config loader, the feature-flag mechanism, the test harness. These have no home directory and therefore no owner and therefore no review. Grade them.

Boundaries. Every place two units meet. HTTP and gRPC APIs, queue topics, webhook endpoints, database schemas shared between writers, the ORM layer, the FFI edge, the browser/server split, the third-party integration, and the in-process module boundary enforced only by convention. A boundary is a unit. It gets its own id, its own findings, and its own score.

Id shape:

module.billing                     a module
module.billing.pricing             a submodule worth grading separately
subsystem.jobs                     cross-cutting machinery
boundary.api.public-v2             a contract with outside callers
boundary.billing-to-identity       an in-process module seam
boundary.db.orders                 a persistence boundary with >1 writer
boundary.ext.stripe                a third-party integration

Ids are validated: lowercase a-z0-9, separated by ., - or _, and nothing else. Write a seam as billing-to-identity, not billing->identity — an arrow is rejected and ponder units exits 1 on the whole file. Put the arrow in label, where it reads properly: "billing → identity".

Set path to the directory or the file that defines the contract — for a boundary, that is the route file, the schema, the interface, or the client wrapper. A boundary with no file that defines it is itself a finding: the boundary exists in people’s heads only.

Enumerating exhaustively

The front matter has the commands. This is what to do with them.

Start from four independent inventories, then reconcile

Each of these sees units the others miss, and the disagreements between them are some of the best findings in the report.

  1. The build graph. Workspace members, modules, projects. What the tooling thinks the units are.
  2. The deploy graph. Dockerfiles, services in compose, Terraform resources, Lambda functions, cron entries, workers. What actually runs as a separate thing, with its own failure domain and its own restart.
  3. The ownership graph. CODEOWNERS, team labels, the directories different people touch in git log --format='%an' --name-only. What humans think the units are.
  4. The change graph. Files that change together. Run the churn command in the front matter, then look at co-change: two directories that always appear in the same commit are one module wearing two names, regardless of what the build graph says.

Reconcile them and write down the disagreements. A directory the build treats as one package, that two teams own, that always changes in three separate pieces, is a module that wants to be split — and you found it by counting, not by feeling.

Boundaries, specifically

Walk this list and enumerate each hit as its own unit:

Coverage is the whole point

coverage < 1.0 blocks stop with no override, which makes under-enumerating the only cheat available. Do not take it. The units most likely to be quietly omitted are exactly the ones that need grading: the subsystem nobody owns, the boundary between two teams, the legacy module everyone routes around.

The eleven dimensions

One finding is one judgment about one (unit, dimension) pair.

Rungs, across every dimension:

The distinction between 3 and 5 in architecture is almost always enforcement. A convention everyone follows is a 3, because it decays the week someone is in a hurry. The same convention with a lint rule, a module boundary, a visibility modifier, or a separate DB role behind it is a 5.

So the mechanical test for a 5 here is: name the mechanism, and name the file it lives in. eslint.config.mjs:41. package.json exports. The GRANT in migrations/0042_roles.sql. The internal/ directory that the Go compiler enforces for you. tsconfig.json project references. A 5 whose evidence is a paragraph in CONTRIBUTING.md, a team agreement, or “everyone here knows not to” is a 3 written by someone who liked the team. Write the 3.

The inverse error costs as much. A structure that looks unusual — a module that does its own thing, a deliberate duplication, a boundary that is coarser than the textbook wants — is not automatically a 2. Ask what enforces it and what it buys. Duplication chosen on purpose, documented, and kept from drifting by a test is a 5 on coupling, not a violation of DRY.

coupling

How far does a change ripple, and can you know that before you make it?

cohesion

Does everything in this unit belong here, and is everything that belongs here in it?

boundary-integrity

Is the boundary real, or is it a suggestion?

dependency-direction

Which way do the arrows point, and can you prove it?

data-ownership

Who is allowed to write this, and does anything stop everyone else?

ubiquitous-language

Does one concept have one name, everywhere?

contract-stability

Can callers depend on this without watching your commits?

testability

Does the shape of the code make the test obvious?

failure-modes

What happens when the thing you depend on is down or slow?

observability

Can you answer a question about one request in production without redeploying?

migration-debt

How many half-finished transitions is this system carrying?

What counts as evidence

Not evidence: “this violates clean architecture”, “microservices would be better”, “this should be a monorepo”, a layer diagram you drew from imagination, or a pattern name used as an argument. Naming a pattern is not the same as naming a cost.

Every must finding must name a cost that has already been paid: a commit that was harder than it should have been, an incident, a bug class, an onboarding complaint, a number from the change graph. Architecture findings without a demonstrated cost are preferences, and they are why architecture reviews get ignored.

What the four perspectives hunt for here

Skeptic → delete

Kill the architecture astronaut. This lens attracts more unfounded findings than any other, because “this could be cleaner” is always true and almost never actionable.

Delete a finding when:

You also audit the top of the scale, which nobody else looks at. Every finding scored 5 must name a mechanism and the file it lives in. A 5 justified by “follows clean architecture”, “well-factored” or “the team is disciplined about this” is a 3 in flattering words — delete it and say in the why that the score was unearned, so Empath can re-grade it honestly.

Builder → edit

Every finding names the first commit. Not the end state — the first commit, the one that could land tomorrow.

Edit until the finding contains: the extraction or the boundary to introduce, the file and symbol it starts from, what stays working during and after, and how the old path is retired.

“Decouple orders from billing” is not a finding. “Extract PricingCalculator from OrderService (order_service.ts:210-390) behind an injected interface; keep OrderService.calculatePrice delegating for one release so no caller changes; then delete it once the 6 call sites move” is.

Decompose every l and xl finding into a first step that ships alone and leaves the codebase strictly better even if the remaining steps never happen. If it cannot be decomposed that way, the finding is a project and should be labelled one.

Keep to your op. edit is prose and evidence: title, detail, recommendation, evidence paths. score, severity, confidence, effort and status are change fields and they belong to Empath. Decomposing a finding therefore lands as an add for the first step plus a change on the remainder — emit both, with a why on each. Do not shrink an xl into an s inside an edit: a finding that quietly changes size between passes is invisible to the thrash detector, which is the only thing watching for a loop that is rewriting rather than converging.

Dreamer → add

You read across modules, which is the only place these findings exist.

Empath → change

Architecture is experienced by people. You re-score and re-rank by who pays.

Pass discipline

Every unit needs at least one finding or it counts as ungraded and blocks stop. For a module that is genuinely well-built, write the finding anyway at score 4 or 5, severity note, naming the specific structural decision that makes it good — those are the templates the rest of the system should copy.