Tooling · Conventions
The Doc Said "Done."
Half of It Wasn't.
Kotlin's own repository-pattern.md said the naming cleanup was finished. It covered the write-side Repository and missed every parallel read-side Query interface — the kind of half-finished fix that survives because nothing automated was checking. Building the check that should have existed immediately found three more real violations elsewhere, and four rounds later, doing the same thing to fifteen more conventions had also proven when to stop.
The root guide is explicit about Repository method names — find<Noun>s for any lookup, single record or list, save<Noun> for writes, no update method. Checking whether the real code actually followed it turned up violations in four of five languages: Go and FastAPI used a bare Save/save with no noun; the Card domain — the second Bounded Context, added later — had a "dedicated findOne plus a separate findAll" shape in Java, Kotlin, Go, and FastAPI that the convention doesn't allow. Only NestJS was clean everywhere.
"Done," According to the Doc
Kotlin's own repository-pattern.md claimed this exact cleanup was already finished. It was — for AccountRepository, the write-side interface. The parallel read-side interfaces — AccountQuery, CardQuery, PaymentQuery, RefundQuery — still had the old names. Nobody had lied; the fix had genuinely landed on one side of a symmetric pair and just never made it to the other. The doc today says so plainly, because it now doubles as its own regression note:
Even if a doc says "done," if an interface's renaming was actually missed (as CredentialQuery.findByUserId once was), it surfaces as a harness FAIL.Asking Why It Kept Recurring
The interfaces were fixed by hand across four language worktrees, verified with a full build and test run each, and pushed. Then came the more useful question: why had this specific, simple, well-documented convention drifted in four languages independently? The harnesses already checked plenty — file placement, layer purity, import direction — but none of them checked exact method-name conventions. check_docs_drift.py checked something adjacent but unrelated: whether a doc's claims match the file tree, not whether a method is spelled the way the doc says it should be. No tool existed that could have caught this, in any language, ever. That was the actual root cause, not carelessness in any one implementation.
The New Rule Proved the Diagnosis on Its First Run
A repository-naming harness rule went into all five languages — including NestJS, which was already compliant, purely as a regression guard against the next drift. A blocklist approach: flag findBy*, a bare findAll, count*, a bare save, a bare delete, on *Repository/*Query interfaces. It caught three more real, previously unnoticed violations immediately — all in the Auth/Credential domain, across Go, FastAPI, and Kotlin. If the tool-gap diagnosis had been wrong, the new rule would have found nothing new to find.
Then: How Many More Rules Like This Exist?
The next question was obvious once the first one paid off — what other conventions were documented but not enforced? Three more rounds followed, adding fifteen structural rules in total — not all of which applied to every language: domain-layer isolation, no cross-aggregate references within a Bounded Context, no direct env-var access outside config modules, aggregate-ID hex format, the exact four-field error-response shape, soft-delete filtering on every query, and more.
Not every rule applied to every language, and forcing one where it didn't fit would have just traded real signal for noise — each language investigated applicability first and skipped or narrowed a rule with a documented reason rather than shipping a false-positive machine. FastAPI skipped an interface/infrastructure-isolation rule because its own docs mandate direct infrastructure instantiation inside Depends factories — there's no DI container to isolate against. Go skipped a no-public-setters rule because Go structs are conventionally all-exported in this codebase; the rule's premise about encapsulation simply didn't hold for the language. Go's own pass through this round did find one more real violation on its own — interface/http importing infrastructure/auth directly, a boundary the new domain-layer-isolation rule was built to catch.
Round three added five more rules and found two more real bugs — FastAPI's PaymentModel and RefundModel were missing a deleted_at column entirely, unlike every other model in the same codebase. It also disproved something a previous round's notes had flagged as a known gap: Java's rate-limit filter, believed still unwired, turned out to have already been fixed for real, in an earlier, unrecorded commit — a reminder that a "known gap" written down once is a snapshot, not a live fact, and needs re-checking against current code before it gets cited again.
Round Four Found Almost Nothing, and That Was the Finding
The fourth round added four more rules across all five languages and turned up zero real code violations anywhere. What it did find were two leftover doc claims — one language's tactical-ddd.md still describing the repository as single-domain, a line nobody had touched since before the second Bounded Context existed — and one honest non-applicability: Go's stack has no ORM at all, so an ORM-autosync rule simply doesn't apply, and got documented as explicitly not applicable rather than forced through.
Round one and two: three to four real violations found per round. Round three: two. Round four: zero code bugs, two stale doc lines. That drop isn't evidence the later rounds were wasted — it's the closest thing to proof that the earlier rounds had actually closed most of the low-hanging cross-language drift this category of check can find. The goal was never to keep finding bugs forever; it was to find out when to stop, and a flat yield curve is the only honest way to learn that.
"The doc said done" turned out to be less a lie than a claim nothing could verify — a self-report with no regression guard behind it, in a codebase with five parallel implementations any one of which could quietly drift back. What changed wasn't just the naming. It was that "done" stopped being something a doc could merely assert.
docs/architecture/repository-pattern.md — the naming convention, and the note explaining why the harness now enforces it · repository_naming.go — one language's version of the regression guard