The most common question CTOs ask about an executable engineering standard is: "isn't that just SonarQube?". No. But not instead of it, either. These tools live at different levels of abstraction and complement each other, they don't compete.

This article is a 12-parameter comparison table and a breakdown of "when to use what".

Summary in 30 seconds

  • SonarQube / ESLint / Detekt — for code-level and style bugs. Regexes, AST analysis, security.
  • Tech lead code review — for architecture and domain. Doesn't scale to a team of 20+.
  • AI skills with a rule corpus — for architecture and domain. Scales. Complement linters, don't replace them.

All three should run at the same time in a mature team. The linter catches an NPE, the skill catches "the event is registered outside the aggregate root", the human looks at "was the aggregate boundary even chosen correctly".

Full comparison table

ParameterSonarQube/ESLintTech lead reviewAI skill with corpus
What it checksstyle, security bugs, complexityarchitecture, domainarchitecture, domain
Depth of understandingsyntax + ASTsemantics + contextsemantics + context (via corpus)
Who executesstatic analyzerhumanLLM agent
Codifiedplugins + configin the headmarkdown in the repo
Reference in the reviewyes (java:S1234)noyes (R-AGG-X4)
Scalesyesno (bottleneck)yes
Review latencyseconds (CI)days (tech lead reads)minutes (claude run)
False positivesmedium (~10–20%)lowmedium (~10–15%)
Understands the domainnoyesyes
Versionedplugin releaseverbal rulesgit diff on the corpus
Opennessopen-source enginein the headteam repo
Rule lifecycleplugin releasesinformallygit, semantic diff

Where they DON'T compete

Take the same PR — an order payment handler:

public void handle(PayOrder cmd) {
    Order order = orders.findById(cmd.orderId()).get();
    order.setStatus(OrderStatus.PAID);
    order.setPaidAt(Instant.now());
    orders.save(order);
    events.publishEvent(new OrderPaid(order.id(), order.total()));
}
func (h *PayOrderHandler) Handle(ctx context.Context, cmd PayOrderCommand) error {
    order, err := h.orders.FindByID(ctx, cmd.OrderID)
    if err != nil {
        return err
    }
    order.Status = OrderStatusPaid
    order.PaidAt = time.Now()
    if err := h.orders.Save(ctx, order); err != nil {
        return err
    }
    return h.events.Publish(ctx, OrderPaidEvent{ID: order.ID, Total: order.Total})
}
async handle(cmd: PayOrderCommand): Promise<void> {
    const order = await this.orders.findById(cmd.orderId);
    order.status = OrderStatus.PAID;
    order.paidAt = new Date();
    await this.orders.save(order);
    await this.events.publish(new OrderPaidEvent(order.id, order.total));
}
from datetime import datetime, timezone

def handle(self, cmd: PayOrderCommand) -> None:
    order = self.orders.find_by_id(cmd.order_id)
    order.status = OrderStatus.PAID
    order.paid_at = datetime.now(tz=timezone.utc)
    self.orders.save(order)
    self.events.publish(OrderPaidEvent(id=order.id, total=order.total))

SonarQube will say:

  • java:S3655Optional.get() without isPresent() — may throw
  • java:S1192 — string literal "PAID" is duplicated (if present elsewhere)

The AI skill ucp-ddd-tactical-review will say:

  • R-AGG-X4 — the event is registered outside the aggregate root
  • R-ENT-X3 — public setters instead of a business method
  • R-EVT-X4events.publishEvent() without transactional guarantees

The tech lead will say:

  • "Should this use case be Order.pay() or an application service PaymentSaga at all? Our payment is asynchronous and comes in through a webhook."

Three levels. Three tools. All three are needed.

When to pick what

Linter only

  • Team < 5 engineers.
  • Simple CRUD project, not domain-complex.
  • No architectural standards in the tech lead's head.

Linter + tech lead review

  • Team of 5–10.
  • There's a tech lead with an established architecture.
  • The architecture rests on people, not formalized.

Linter + AI skills + tech lead

  • Team of 10+.
  • Several services, risk of drift between them.
  • Architectural patterns are established enough to be codified.

Linter + tech lead (without AI skills)

  • Early prototype, everything changes weekly.
  • The team doesn't trust AI reviews.
  • The architecture hasn't settled yet.

What all three have in common

All three are review tools, not replacements for responsibility. The linter doesn't write the code for you. The tech lead doesn't sign off the PR for the developer. The AI skill doesn't make architectural decisions. They are all amplifiers, not substitutes.

The review quality at any of the three levels depends on the quality of the underlying standards:

  • ESLint without a good configuration is noise.
  • A tech lead without established principles is subjective whims.
  • An AI skill without a good rule corpus is a generic review at the level of "improve readability".

A high-quality rule corpus is what sets a mature team apart from blindly copying someone else's practices.

What AI skills do that linters don't

There's one class of rules that traditional linters fundamentally can't reach:

  1. Rules about the domain layer. "A value object name is a noun in the singular." The linter doesn't know what a value object is and which name is "domain" and which isn't.
  2. Rules about boundaries. "One use case modifies one aggregate, the rest through events." The linter doesn't know where the aggregate boundaries run in your domain model.
  3. Rules about intent. "The class name matches a domain term from the ubiquitous language." The linter doesn't know the ubiquitous language.
  4. Rules about context. "At Level 2, mapping goes through a dedicated mapper class; at Level 3, through a generated one." The linter doesn't know the maturity level and doesn't select rules based on it.

An LLM can do all of this because it reads the prose of the rules alongside the prose of the code. For an LLM, "domain term" versus "not domain" is a matter of context, not an AST node.

What linters do that AI skills don't

And the reverse:

  1. Speed. The linter scans 100K lines in 3 seconds. A skill takes tens of seconds per PR.
  2. Determinism. The linter gives the same result on the same code. An LLM — with variations.
  3. Low-level bugs. NPE, deadlock, off-by-one. That's the job of an AST analyzer.
  4. Security scanning. SQL injection, XSS, hardcoded secrets — the patterns are more well-defined.

Next