This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Korro is a Gradle plugin (Kotlin/JVM), published as io.github.devcrocod.korro, that injects Kotlin function bodies into .md/.mdx docs via <!---FUN ...--> or {/*---FUN ...--*/} directives. Consumer-facing syntax and the DSL are in README.md; the 0.1.x→0.2.0 migration is in MIGRATION.md. Read both before changing the directive parser or the extension DSL — they are the downstream contract.
Gradle wrapper (9.4.1):
./gradlew build— compile and assemble both modules../gradlew :integration-tests:test— GradleTestKit + golden-file tests underintegration-tests/fixtures/. This is the only meaningful test suite in this repo../gradlew publishToMavenLocal— install both artifacts to~/.m2/for consumer testing. Both must be installed together:korroAnalysisRuntimeresolveskorro-analysisat the plugin's own version at task-execution time../gradlew -Prelease build— release-versioned artifact. Without-Prelease,detectVersion()in the rootbuild.gradle.ktsappends-dev(or-dev-<build.number>) to the version ingradle.properties../gradlew :korro-analysis:shadowJar— build only the fat jar../gradlew publishPlugins— publish to the Gradle Plugin Portal (requires credentials).
The korroGenerate / korro / korroCheck tasks the plugin registers are not runnable from this repo's root — only from a consumer project or an integration-tests/fixtures/* fixture.
Two modules separated by a Gradle worker boundary.
Runs in the Gradle daemon classloader. No Analysis API imports at compile time — only compileOnly(gradleApi()) + implementation(kotlin("stdlib")). Contains KorroPlugin, KorroExtension, the three tasks, and the markdown directive parser (Korro.kt).
The parser lives here, not in korro-analysis, because <!---…--> / {/*---…--*/} parsing doesn't need the Analysis API. Per-file marker form is selected by extension through DirectiveSyntax: .mdx uses JSX-expression comments (required — Mintlify/Docusaurus reject raw HTML comments); everything else uses the HTML-comment form.
Analysis code is pulled in at task-execution time: KorroPlugin creates a detached korroAnalysisRuntime configuration with a dependency on io.github.devcrocod:korro-analysis:<pluginVersion>, and tasks submit work via WorkerExecutor.classLoaderIsolation { classpath.from(korroRuntimeClasspath) }.
Task shape to preserve:
korroGenerate(@CacheableTask) writes out-of-place tobuild/korro/docs/.korroextendsCopy(neverSync), depends onkorroGenerate, and copies its output ontodocs.baseDir. This is the only source-mutation point. Must stayCopy:docs.baseDiris typically the repo or project root and contains many files Korro does not manage —Sync's delete-unknown semantics would wipe the working tree.korroCheck(@CacheableTask) regenerates intobuild/korro/check/, diffs against the source tree, and fails the build with the first differing line per file. CI entry point.- Every task has an
@Input korroPluginVersionso cached outputs invalidate on plugin bump (which is also the Analysis API bump).
Runs inside the worker's isolated classloader. Bundles the Kotlin Analysis API (K2 standalone), low-level FIR, and the IntelliJ platform. com.intellij.* and org.jetbrains.kotlin.* are intentionally unrelocated — the Analysis API is already uniquely namespaced, and relocating it breaks reflection lookups inside the platform.
- One
StandaloneAnalysisAPISessionperKorroWorkAction.execute()call, disposed in atry/finally. Do not calldisposeGlobalStandaloneApplicationServices()— it's a one-shot that invalidates all future Analysis API use in the JVM.classLoaderIsolationgives a fresh classloader per task run, so singletons are reloaded naturally. - FQN resolution is two-tier:
byFqn(exact full-FQN map) is the primary path, used for everyIMPORT-qualified candidate and for anyFUN/FUNSvalue that already contains a..byShortName.singleOrNull()is the bare-name fallback and fires only when noIMPORTdirectives are in effect — IMPORT is authoritative when present, so a bareFUNunderIMPORTwill not slide over to a same-named declaration in an unrelated package.
KorroWorkParameters is serialized across the classloader boundary (even under classLoaderIsolation, Gradle serializes parameters). All fields must stay Serializable — Set<File>, primitives, strings, and the SamplesGroup DTO only. No Project / Task / Logger references.
- Korro's own version lives in
gradle.properties(version=...). Both subprojects inherit it viasubprojects { version = rootProject.version }in the rootbuild.gradle.kts. At runtime the plugin reads it from a generatedMETA-INF/korro-gradle-plugin.propertiesresource (KorroPlugin.readKorroPluginVersion). - Every other version lives in
gradle/libs.versions.toml. The catalog is the single source of truth — do not hard-code versions in subproject scripts; add to the catalog and reference aslibs.*/libs.plugins.*. libs.versions.kotlin— pinned Kotlin / Analysis API version.libs.versions.kotlinLanguage— KotlinlanguageVersion/apiVersionused to compile Korro itself; unrelated to the bundled Analysis API. JVM target is hard-coded to17in the rootbuild.gradle.kts.
These are contracts for every consumer's docs; breaking any of them silently breaks downstream projects.
- Directives start at column 0 after
String.trim().parseDirectivereturnsnullotherwise. - Three dashes to open, two to close.
<!---NAME VALUE-->for.md(and anything non-.mdx);{/*---NAME VALUE--*/}for.mdx. Do not collapse the open marker to two dashes — that becomes a standard HTML/MDX comment, and consumer docs rely on the distinction. - Directive name regex is
[_a-zA-Z.]+. Broadening it changes parsing for every consumer. - First
IMPORTwins when several IMPORT prefixes resolve the same short name (firstNotNullOfOrNullover theimportslist). Theimportslist holds only explicit IMPORT prefixes — do not re-introduce an implicit empty seed, since that lets the resolver's bare-name uniqueness fallback hijack IMPORT-scoped lookups. KtNamedFunction,KtClassOrObject, andKtPropertyare validFUN/FUNStargets. Enum entries, type aliases, local declarations, and.ktsscripts are not; resolving to a non-target produces a diagnostic, not a silent empty snippet. Class/object/property targets rely on//SampleStart///SampleEndmarkers inside their body for non-empty output.behavior.ignoreMissing=falseis the strict-by-default contract. Don't silently lower severity on unresolved references without an explicit opt-in.