Skip to content

Kotlin port: var-kotlin authoring facade + var-kotest adapter over the Java engine - #7

Merged
aslakhellesoy merged 13 commits into
mainfrom
worktree-dreamy-waddling-newell
Jul 2, 2026
Merged

Kotlin port: var-kotlin authoring facade + var-kotest adapter over the Java engine#7
aslakhellesoy merged 13 commits into
mainfrom
worktree-dreamy-waddling-newell

Conversation

@aslakhellesoy

Copy link
Copy Markdown
Contributor

Summary

The Kotlin port — unlike TypeScript→Python→Java, this does not re-port the pipeline: it layers an idiomatic Kotlin authoring API on the existing, conformance-green Java engine (the design decision the adding-a-language-port skill required writing down; see the spec).

Authoring looks like this — one top-level val per .steps.kt file, no class, state as the handler receiver:

data class Ctx(val cukes: Int = 0)

val steps = defineState(::Ctx) {
    context("I have {int} cukes") { n: Int -> copy(cukes = n) }
    action("I eat {int} cukes") { n: Int -> copy(cukes = cukes - n) }
    sensor("I should have {int} cukes left") { cukes }
}

What's in the box

  • java/var-kotlin — the com.oselvar.varkt facade: defineState returns an inert, replayable StepDefinitions (no static accumulator — registration only happens when the runner replays the block against a fresh Registrar); StateBox bridges bare data classes into the engine's C extends State bound; suspend handlers from day one via runBlocking; custom parameterTypes.
  • java/var-kotestVarSpec : FunSpec(): subclass it, point the shared three config keys at your specs/steps, one Kotest test per planned example; failure rendering delegated to Render.renderFailure (span-anchored).
  • var-junit works unmodified — the existing JUnit Platform engine drives Kotlin-authored steps end to end (proven by EngineTestKit smoke tests).
  • Java-side enablers (Kotlin-agnostic): @RegistrarGlue so StackWalker source locations skip DSL glue frames and point at the author's .steps.kt; StepLoader now loads public static no-arg factory methods returning StepDefinitions (what a top-level val compiles to) and rejects two defineState registrations per source file.
  • Conformance: 12 new *.steps.kt fixtures in the shared corpus, registry stage gated byte-for-byte against the committed goldens (goldens untouched); stem-compatible file names (numerals.steps.kt) need zero changes to Conformance.fileStem.

Design notes (recorded in the spec's Risks-RESOLVED section)

  • The flagged zero-parameter-lambda overload ambiguity materialized (K2 rejects sensor("…") { cukes } with same-scope arity overloads). Resolved with a member/extension split (members win resolution for parameterless lambdas) plus arity-tolerant handler adapters (surplus captured args dropped, TS semantics) — the interview-approved API compiles and runs verbatim. Consequence: authors import context/action/sensor alongside defineState (IDE auto-import).
  • @file:RegistrarGlue on the DSL file annotates the file-facade class so extension-registered steps still attribute to the author's file.

Testing

Full reactor (mvn -f java/pom.xml clean test): 351 tests, 0 failures across all six modules, including 12/12 registry conformance, both engine smokes (JUnit + Kotest), and the failure-rendering path through the real Kotest engine.

Docs: design spec (docs/superpowers/specs/2026-07-01-kotlin-facade-design.md, with resolved risks) + task plan. Known follow-ups (from the final review, deliberately post-merge): Kotest diagnostics parity with var-junit's ReportEntry, kotlin-maven-plugin pluginManagement + kotlin-reflect/coroutines pins, shared relPosix helper, CLAUDE.md repo-layout addition of java/.

🤖 Generated with Claude Code

https://claude.ai/code/session_017h1WEs7ReorF43DQsu4u4K

Aslak Hellesøy and others added 13 commits July 2, 2026 00:14
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017h1WEs7ReorF43DQsu4u4K
The plan's single-class overload ladder was ambiguous for parameterless
lambdas (K2: 'Overload resolution ambiguity' between the arity-0 and
arity-1 sensor/context/action overloads), and a strictly arity-matched
SAM registration would break the approved zero-parameter sensor at
execution time (Execute.invokeHandler matches by exact parameter
count). Resolved with two internal changes that keep the approved
author API verbatim:

- zero-parameter overloads are StepsScope members, capturing arities
  are top-level extensions — members win resolution for parameterless
  lambdas, extensions catch parameter-declaring ones
- handlers register through arity-tolerant ContextAdapter/SensorAdapter
  shims exposing one apply overload per call shape, dropping surplus
  captured arguments (TS-facade semantics); declaring more parameters
  than the step supplies raises an authoring error

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017h1WEs7ReorF43DQsu4u4K
… package

Closes the review finding that DefineStateTest (same package as the DSL)
cannot catch a missing-import regression for the extension-function
overloads: real .steps.kt files live in their own packages and need
explicit imports of context/action/sensor alongside defineState.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017h1WEs7ReorF43DQsu4u4K
… duplicate per-file defineState

Generalize StepLoader to resolve classes via reflection to step-definition load units:
either (a) classes implementing StepDefinitions (instantiated, original path), or
(b) classes exposing public static no-arg methods returning StepDefinitions (each
invoked; name-sorted for determinism). This is the plain-Java shape of what a Kotlin
top-level `val steps = defineState(...)` compiles to (a file-facade class with a
static getter), but the check is Kotlin-agnostic.

Add duplicate-file detection: when two load units' steps report the same
expressionSourceFile, reject with "one defineState per step-definition file" error,
closing the latent silent-overwrite risk (Tasks 6–9 rely on static factories).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017h1WEs7ReorF43DQsu4u4K
…t fixtures

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017h1WEs7ReorF43DQsu4u4K
VarSpec extends Kotest's FunSpec: discovers .md specs via var-runner's
Discovery, loads steps via StepLoader, plans via Run.planSpec, and
registers one Kotest container per spec file with one test per planned
example. Delegates discovery/loading/planning/failure-rendering
wholesale to var-runner; no pipeline logic in the adapter itself.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017h1WEs7ReorF43DQsu4u4K
Also opts in to @KotestInternal on the registration guard's Spec.tests()
call so the build stays warning-free (Task 9 review minor).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017h1WEs7ReorF43DQsu4u4K
Final whole-branch review findings:
- VarSpecFailureTest drives a deliberately failing spec through the real
  Kotest engine via EngineTestKit and asserts the example fails with an
  AssertionError carrying Render.renderFailure's span-anchored text
  (note: Kotest reports the per-file container as a TEST-type descriptor
  that succeeds independently of its failing child)
- spec doc: correct fixture package (varkt), the sourceDirs-not-build-helper
  conformance wiring, the per-module kotlin-maven-plugin config, and the
  false package-scanning claim (StepLoader resolves FQCNs individually)
- VarSpec javadoc: diagnostics defer matches var-pytest only (var-junit
  surfaces diagnostics via ReportEntry since b7b093b)
- VarSpecSmokeTest: document Surefire's 'Tests run: 0' Kotest counting quirk
- DefineState: drop HandlerAdapter's unused type parameter

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017h1WEs7ReorF43DQsu4u4K
@aslakhellesoy
aslakhellesoy merged commit 4fd8b8e into main Jul 2, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant