Skip to content

Kotlin Standards Plugin

The Kotlin Coding Standards, enforced rather than described. A Gradle plugin published from kotlin-standards-gradle-plugin and applied by any Arda Kotlin repository.

  • Plugin id — cards.arda.standards.kotlin
  • Artifact — cards.arda:kotlin-standards-gradle-plugin, published to GitHub Packages
  • Consumers — print-module, operations

This page describes what the apparatus is. Automated Checks is the page for adopting and using it; the design record and decision log record how it came to have this shape.

Two tiers, because two kinds of claim need different instruments

Section titled “Two tiers, because two kinds of claim need different instruments”
EngineVerdict depends onExample claims
Tier Adetekt, on its own classpathone file!!, getOrThrow(), a naked UUID in a declaration
Tier BArchUnit and the package graph, in processthe whole programpackage cycles, layer direction, the protocol boundary

The split is not organisational. A file-local rule can be decided by reading one syntax tree; a structural rule cannot be decided that way at all, because the property it asserts — that no package imports a package that imports it back — is not visible from inside any single file. Tier B therefore reads compiled output, which is also why a Tier B finding is anchored to the file the class is actually in.

Both tiers write the same report shape, and one reader renders them against the standard that defines them. That is what lets a finding cite a section a developer can open instead of repeating a fixed tool message — and building those citations is what first revealed that three enforced rules had no published claim to cite at all.

Every rule appears in every report, in exactly one of three states.

BLOCKING / CARRIED / ADVISORY — findings
CLEAN — ran, found nothing
SKIPPED — did not run, and why

This is the property the whole design is built around: a rule that ran and found nothing and a rule that never ran must never look alike. Every silent miss this apparatus has met came through that gap. The donor repository catalogues six independent forms of silent non-execution — a stale service-registration file, a rule with no compile classpath, a daemon-cached classloader, a task skipped as up-to-date, source roots that judge generated code, an unresolvable ratchet base — and each of them produces a report that looks exactly like a clean tree.

Two consequences are worth stating because they are easy to get wrong:

  • A disabled rule reports SKIPPED, never CLEAN. This was verified by experiment rather than assumed, and the experiment is what exposed the hole: with a rule switched off through a configuration overlay, the report listed it under “ran over the whole tree and found nothing”.
  • A rule whose subject is absent also reports SKIPPED. A repository with no !! and a repository with no way to recognise one are different states and must read differently.

Nothing here is a way to be quietly excused from anything. That is a property the design has, not a claim it makes.

AxisQuestionWho declaresHow it is audited
FacetsWhat exists here?The repository, in repository.propertiesThe declaration is exhaustive, and the plugin checks it against the code
ScopeHow hard is it held?Plugin default, repository overridePrinted in every report, with the default beside it
Site departuresWhy this line?The author, in a @SuppressVisible in the diff, inventoried, and failed if it waives nothing

Scope is repository-local, and deliberately so. One repository ratchets a rule because it carries a standing backlog; another holds the same rule at tree because its baseline is empty. Same rule, same citation, opposite posture, both correct. What is not repository-local is a rule’s claim, guidance and citation — those name the Kotlin Coding Standards, which are one document for the organization.

The distinction that makes the dial safe is that a scope acts on findings while an exemption acts on whether the rule looks. An advisory rule still prints everything it found: the knowledge is preserved and merely deferred. An exemption produces no finding at all, so nothing is deferred and nobody can count what it hid. That asymmetry is why none is a scope rather than an exemption list, and why disabling a catalogued rule through detekt’s own configuration is refused with an error.

The plugin is not a bag of rules. It is the apparatus, and the rules are its payload. The catalog, the roster check, the liveness check and the report all operate over platform rules ∪ a repository’s own rules.

Tier A needed no work: detekt’s plugin model already loads a consumer’s rule set beside the platform’s. Tier B was the sharp case, because it was not a detekt rule set at all — it was a runner with a literal roster in its own source, verified by parsing that source file’s text. That is not extensible by anyone but the plugin’s author, and it is not a mechanism a second contributor can reason about. The roster is now discovered through a ServiceLoader, so a repository’s rules are catalogued, counted, scoped and reported on the same terms as the platform’s.

The invariants that protect the roster are enforced by the plugin rather than copied into consumers: a repository that adds a rule without a catalog entry, or without a liveness fixture, fails its own build without having had to know the requirement existed. A copied invariant drifts — two rules once held two exclusion lists that a comment asked to agree, in two repositories, for as long as both had existed, and they did not.

Contributing a rule upward needs no extension point at all. It is a pull request against the plugin, because a rule going up is a claim about the platform and deserves review, while a rule staying local is a claim about one repository and does not.

Twenty-five catalogued rules, all at tree by default — the platform’s claim about its own rules, not a prediction about any repository. Four public tasks: a survey that never fails, a gate that enforces scope, a suppression inventory, and a stale-waiver check that the gate depends on. The full list, with each rule’s claim and the section it cites, is generated from the file the build itself reads and published in the repository’s rule catalog.

Version-specific reference material — the key-by-key configuration reference and the migration guides — lives with the artifact rather than here, because it changes when the artifact does.


Copyright: (c) Arda Systems 2025-2026, All rights reserved