Tooling · Testing
A Rule That Has Only
Seen Two Inputs
A lint rule that has only ever passed on the inputs it was written against hasn't been shown to be generic. It has only been shown to work on those inputs. The way to find out is to build an input it has never seen, on purpose, and run the rule against it.
My example project implements the same backend design in five languages side by side, and it comes with an architecture checker, a script that statically checks the code against the documented architecture rules and scores it. Two of its rules had checked out clean for months, and every domain they had ever seen was one of the same two example domains, Account (bank accounts) or Card (payment cards). Whether they would hold on any other domain had never been tested directly. Building a third, deliberately unrelated domain and running the rules against it surfaced two false positives hiding in plain sight. A separate check then confirmed that the rule meant to catch a real mistake still did.
Grep Said It Was Fine
The obvious first check was searching the checker's own rule implementations for hardcoded account/card strings, and it came back almost clean; the only hits were inside comments giving examples. By that measure, the checker was already domain-agnostic. But "doesn't mention the domain by name" and "works correctly on a domain it's never seen" are different claims, and only one of them can be tested by reading the rule code.
So Build a Domain the Rules Have Never Seen
The project already documents its own answer to "how do you add a new domain": docs/reference.md's reference implementation template. Following it literally, in a scratch copy, produced an Order domain with its own CQRS CommandHandler/QueryHandler pair, its own domain event, and (the detail that mattered) its own dedicated OutboxRelay, unrelated to Account or Card in every way except following the same rules. Then the checker ran against it as it would against any other code.
The score moved from 98 to 100, which is a good number in the wrong way to be interesting on its own. What was interesting is that it moved because two rules turned out to be flagging code that was correct.
Two Rules, Two Kinds of Blindness
The rule that checks domain events against the Outbox (domain-event-outbox.evaluator.ts) collected every domain-event name across the whole application into one set and required each individual *-outbox-relay.ts file to cover the entire set. That's backwards from how the code is designed: every domain owns a relay that handles only its own events. With one domain in the codebase, "the app-wide event set" and "this domain's event set" are the same set, so the rule had never once been wrong. Add a second domain with its own relay, and it starts failing correct code by construction. The rule itself was only a few days old, written when the codebase still had one domain to write it against.
The pagination rule had a plainer bug: it regex-scanned an entire Repository file for /data|items|result:/ and flagged any hit, anywhere. Order has a legitimate items field (an order's line items, nothing to do with pagination), and the regex couldn't tell the difference. Account and Card simply never happened to have a field with that name, so nothing had ever forced the distinction.
Neither bug is visible from reading either rule in isolation, and neither is a matter of the rule containing wrong logic in any local sense. Both only exist once more than one domain is in the picture: a global set that should have been scoped per-domain, and a field name that happened not to collide until something new was built. Grep-auditing for hardcoded domain strings can't find either, because neither rule mentions a domain name anywhere.
Both were filed and fixed the same day, one commit: the event set narrowed to each domain's own directory instead of the whole app, and the generic-key check narrowed to the literal return-type shape a paginated response has (Promise<{ items: T[]; count: number }>) instead of scanning the whole file. Four regression fixtures went in alongside the fix — a good and a bad case for the multi-domain scoping, a good and a bad case for the field-name collision.
Checking That the Fix Didn't Also Remove the Detection
Narrowing a rule to stop a false positive is the kind of change that can also stop catching the real thing without anyone noticing. The codebase happened to have a real mistake sitting in it at the time (three error codes missing from an enum, entirely unrelated to either fix), and the error-code rule (error-handling.error-code.enum-count-mismatch) caught it both before the narrowing and after. The false positives were gone; the detection wasn't.
Untested Is Not the Same as Generic
"How many rules only Account and Card have ever exercised" turned out to be the more useful question than "how many rules mention Account or Card by name." Low string-coupling and correct generic behavior are not the same claim, and only one of them survives contact with a second domain. Both bugs shared a precondition neither static reading nor the grep audit could produce on its own: they only exist once more than one domain coexists in the same codebase. The practice that followed from this is mechanical — a new or changed rule now gets run once against a domain deliberately unrelated to whatever prompted it, not just against the two it was written against, before it's trusted to be generic rather than merely untested.
implementations/nestjs/docs/reference.md (the new-domain template the Order build followed exactly, in my example project that implements the same backend design in five languages) · pagination.evaluator.ts (the fixed generic-key check, scoped to the response type literal)