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”| Engine | Verdict depends on | Example claims | |
|---|---|---|---|
| Tier A | detekt, on its own classpath | one file | !!, getOrThrow(), a naked UUID in a declaration |
| Tier B | ArchUnit and the package graph, in process | the whole program | package 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.
The report contract
Section titled “The report contract”Every rule appears in every report, in exactly one of three states.
BLOCKING / CARRIED / ADVISORY — findingsCLEAN — ran, found nothingSKIPPED — did not run, and whyThis 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, neverCLEAN. 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.
Three axes, each audited
Section titled “Three axes, each audited”Nothing here is a way to be quietly excused from anything. That is a property the design has, not a claim it makes.
| Axis | Question | Who declares | How it is audited |
|---|---|---|---|
| Facets | What exists here? | The repository, in repository.properties | The declaration is exhaustive, and the plugin checks it against the code |
| Scope | How hard is it held? | Plugin default, repository override | Printed in every report, with the default beside it |
| Site departures | Why this line? | The author, in a @Suppress | Visible 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 extension surface
Section titled “The extension surface”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.
What ships
Section titled “What ships”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.
Related
Section titled “Related”- Automated Checks — adopting it, reading a report, tuning it, writing rules.
- Kotlin Coding Standards — what it enforces.
- DAG Package Discipline — the structural claims Tier B judges.
- Design and Decision Log — how it was arrived at.
Copyright: (c) Arda Systems 2025-2026, All rights reserved
Copyright: © Arda Systems 2025-2026, All rights reserved