Claude Cursor Skill

skmtc-lang-kotlin

The Kotlin target-language layer for Skmtc generators (@skmtc/lang-kotlin): base factories, KtSnippet, the seven entity kinds, packages-from-paths imports, the head+value render model, KtAnnotation and the composition classes, sanitization and @SerialName placement, plus the curr

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download skmtc-skmtc-deno_docs_skills_skmtc-lang-kotlin-e3abffc.zip · 19 KB
skmtc/skmtc 19 0 forks Apache-2.0 Updated 8d ago
Part of skmtc/skmtc — 12 skills

Install

skills CLI npx skills add https://github.com/skmtc/skmtc/tree/main/deno/docs/skills/skmtc-lang-kotlin
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install skmtc-skmtc@llmmart
Git git clone https://github.com/skmtc/skmtc.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole skmtc/skmtc collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

The Kotlin layer (@skmtc/lang-kotlin)

Read skmtc-generator first.

Drift warning. The API of record is the workspace skmtc/deno/lang-kotlin and its tests. The shipped gen-kotlin-* generators predate the 0.9.11 flattening: they call new KtAnnotation('Name', [args]) positionally and import isKtAnnotated/isKtSupertyped (no longer exported; supertype clauses now render inline in the value). Clone their structure only; take call shapes from THIS skill's example (§8), which is pinned byte-for-byte against the engine by lang-kotlin/src/skill-example.test.ts.

1. Declaring the language

Same pattern as TypeScript — the import graph declares it. Two factories: toKtModelProjectionBase, toKtOasOperationProjectionBase; snippets extend KtSnippet.

export const KtModelBase = toKtModelProjectionBase<EnrichmentSchema>({
  id: denoJson.name,
  toEnrichmentSchema,
  toIdentifierName({ refName, enrichments }) {
    return enrichments?.subject?.name ?? capitalize(camelCase(refName))
  },
  // Kotlin's identifier KIND depends on schema shape → may read context
  // (runs only on cache-miss; the NAME stays pure):
  toIdentifierType(refName, context) {
    return { type: toShape(context, peekSchema(context, refName)) }
  },
  toExportPath({ refName, enrichments }) {
    const name = enrichments?.subject?.name ?? capitalize(camelCase(refName))
    return join('@', ...enrichments.generator.basePackage.split('.'), `${name}.generated.kt`)
  }
})

The export path's directory segments ARE the Kotlin package (§4). Make basePackage a required generator-scope enrichment with no default; validate segments with isKtIdentifierName + ktHardKeywords. Put the shape dispatch (object+props → data-class; string+enums → enum-class; qualifying discriminated union → sealed-interface; else typealias) in ONE deterministic function read by both toIdentifierType and the constructor, so kind and value can't disagree.

2. Register shapes — Kotlin differences

Same three shapes as TS (projection own-file / registerInto / snippet with required destinationPath), plus defineAndRegister (no cache check; no noExport — visibility is the identifier's fact: pass exported: false to the factory). Compile-time differences: no reExports field (Kotlin has none) and no type tag on imports (no type-only imports). custom renders above the package directive.

3. Identifier kinds

Kotlin output has seven entity kinds (KtEntityType): class, data-class, enum-class, interface, sealed-interface, typealias, val — factories createClass, createDataClass, createEnumClass, createInterface, createSealedInterface, createTypeAlias, createValue (only createValue takes typeName; exported: false renders private ). Deferred kinds (object, fun, var) make toKtEntityType throw — deliberately loud. Kind does NOT affect import form. The engine's type is an opaque string: isKtEntityType narrows it to the vocabulary above, and isKtIdentifier narrows a neutral IdentifierBase back to KtIdentifier.

4. Emitted-import rules

  • Packages from paths: @/com/example/api/User.generated.kt → package com.example.api. Segments are validated, never sanitized — a keyword or invalid segment throws (fix the path policy).
  • One import pkg.Name per symbol (no brace grouping), as aliases, rendered sorted (determinism, not style).
  • Same-package suppression is central: register imports unconditionally; KtFile drops same-package ones at render.
  • Importing from the default package throws (root-level artifact referenced from a packaged one = path-policy bug).

5. Render model: head + value

Assignment kinds (typealias, val): <head> = <value>. Declaration kinds: <head><value> — the value renders everything after the name: parameter list (parens included), inline : Parent clauses, { … } bodies; an empty value yields the bodyless idiom (sealed interface Animal).

Two things ride on value-carried protocols (the neutral Lang signature has no slot for them): KtAnnotated (annotations: KtAnnotation[], strict — string look-alikes are silently dropped) and KtDocumented (description, guard isKtDocumented, rendered as KDoc above the annotations). The mirroring gotcha: the Driver wraps the PROJECTION as the definition's value, so mirror both onto the projection — canon is reference assignment in the constructor (this.annotations = this.value.annotations — one array, two names; never copy) — or class-level annotations and KDoc silently vanish.

6. Composition classes (current API)

  • KtParameterList(parameters) — parens included; each { name, type: Stringable, nullable?, defaultValue?, annotations?, visibility? } renders as an indented val, annotations one per line.
  • KtPrimaryConstructor({ parameters, modifiers? }) — modifiers force the explicit constructor keyword.
  • KtFunctionSignature({ name, parameters, returnType?, annotations?, body? }) — abstract by default, expression body only.
  • KtAnnotation({ context, name, args?, target?, packageName?, destinationPath }) — a registering leaf: with packageName it registers its own import (register unconditionally; suppression handles same-package). args are pre-quoted (['"user_id"'], ['Foo::class']). target is the use-site target (KtAnnotationTarget: field/get/set/…) rendered as @field:JsonAnySetter — the imported symbol stays the bare name. Needed on a constructor val, which is parameter/property/field/ getter at once: Jackson's catch-all pair is @field:JsonAnySetter + @get:JsonAnyGetter, and without targets both annotations land on the parameter, where Jackson never looks. (Shipped in lang-kotlin 0.10.0, 2026-08-04 — pre-target versions cannot express use-site targets at all.)
  • withDescription(value, { description }) — KDoc.

7. Sanitization and @SerialName

sanitizePropertyName(name): plain → unchanged; hard keyword or invalid → backticked; JVM-unescapable characters → throws ("rename + @SerialName"). Renames are NOT its job — serialization annotations handle wire-name mismatches, and the two compose: decide the annotation by comparing the unescaped chosen name with the wire key (`object` needs no @SerialName; user_id→userId does). Only the 28 hard keywords escape; soft/modifier keywords (value, data, sealed) are legal identifiers. Canonical pairing: sanitizePropertyName(camelCase(key)).

8. Worked example — kotlinx data class (current API, engine-pinned)

Per-property loop inside the data-class value snippet:

const propertyName = sanitizePropertyName(camelCase(key))
const annotations: KtAnnotation[] = []
if (propertyName.replaceAll('`', '') !== key) {
  annotations.push(new KtAnnotation({
    context, destinationPath,
    name: 'SerialName', packageName: 'kotlinx.serialization', args: [`"${key}"`]
  }))
}
parameters.push({
  name: propertyName,
  type: value,                     // the SNIPPET — never `${value}`
  defaultValue: isRequired ? undefined : 'null',
  annotations
})
// this.parameterList = new KtParameterList(parameters)
// class-level: this.annotations = [new KtAnnotation({ context,
//   destinationPath, name: 'Serializable', packageName: 'kotlinx.serialization' })]
// projection mirrors by REFERENCE: this.annotations = this.value.annotations

Renders (verified byte-for-byte through the engine):

package com.example.api

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
data class User(
    @SerialName("user_id")
    val userId: String,
    val name: String,
    val email: String? = null
)

The type expression is the single owner of ?; the parameter layer only adds = null. Passing `${value}` instead of the snippet strands its registered imports and synthesized siblings — the file breaks far from the cause. Serialization flavor is confined to the value files (data class / enum entries / sealed interface): a Jackson/Moshi sibling generator swaps annotation construction there only.

8b. Normalized models — KNOWN ENGINE GAP (verified 2026-08-03)

The head+value model means a Kotlin value renders differently in TYPE position (Map<String, Any?>) and DECLARATION position (a parameter list). Core's generic insertNormalizedModel glues the identifier head to the value's type-position toString() — which for an inline OBJECT schema renders invalid Kotlin: data class XMap<String, Any?>. The engine gap is real, but the SOLUTION does not wait for it — every mature Kotlin generator solves inline objects the same way:

  • Named $ref schemas are unaffected — insertModel and the ref path work correctly.
  • An inline NON-object schema normalizes fine as a typealias-shaped value.
  • An inline object is SYNTHESIZED as a named sibling declaration and referenced by name — the retired gen-kotlin-kotlinx pattern (KtObjectValue, skmtc-generators history at 2c24a65) rebuilt WITHOUT its naming-hint threading: the name derives from the schema's own stackTrail (toSynthesizedName.ts in gen-kotlin-jackson — anchor on the components/paths landmark frames, never absolute indices; classification is POSITIONAL: properties consumes the following frame as a literal key, so a property named properties/schema/items can never be mistaken for trail structure), so every construction path — including peers arriving through insertNormalizedModel — lands on the same name with NO parameter added to the router contract. Names are NOT collision-free: claim via the document-wide registry (claimSynthesizedName, gen-kotlin-jackson synthesizedNames.ts) BEFORE declaring — it throws per-item when the name collides with a component-derived class name (Kotlin's redeclaration scope is the PACKAGE, not the file) or with a different position's claim (camelCase-convergent keys), and returns reuse for a same-position re-walk. On 'declare', defineAndRegister the sibling and render only the NAME. Type position then always holds a name or a map — never property structure. This is also how OpenAPI Generator solves it (inline schemas hoisted to named components before generation). Widening a known shape to Map<String, Any?> is capitulation, not a solution — it discards the type the schema gave you. Inline string enums synthesize the same way (enum class sibling).
  • Never fabricate a refName or drive the peer's identity statics to force a declaration into existence — that is the two-doors rule (skmtc-generator §4), and the result couples you to the peer's private snippet shape.

8c. Discriminated unions — sealed interfaces (shipped 2026-08-04)

Kotlin has no union type; a QUALIFYING discriminated union becomes a sealed interface (gen-kotlin-jackson is the worked example; ancestry: the retired kotlinx machinery at skmtc-generators 2c24a65, stale call shapes). Predicate (shape.ts isSealedUnion, part of the shape dispatch): discriminated, ≥2 members, every member a $ref to an object-with-properties, and every member keeps ≥1 parameter AFTER discriminator omission. Everything else renders the honest wire type (JsonNode for Jackson), never Any.

  • The inversion scan. OpenAPI points parent → member; Kotlin declares member → parent (data class Dog(...) : Pet). Memoization makes build order arbitrary, so membership must be known BEFORE any construction: one document-wide scan over components.schemas, memoized per document via WeakMap, mapping member refName → claims. Claims store the parent's real RefName; the consumer derives the display name via context.toModelContentSettings — never a copy of the naming policy, never a fabricated refName.
  • Parent side: an empty-body value (toString() returns '' → the bodyless idiom) carrying @JsonTypeInfo(use = NAME, include = PROPERTY, property = "<discriminator>") + @JsonSubTypes(Type(value = Dog::class, name = "dog"), …) via the KtAnnotated protocol — mirror annotations AND description on the projection by reference. Each subtype entry holds the walked member ref SNIPPET, so member models build and imports stitch through the normal chain. Tags: discriminator.mapping key pointing at the member, else the member's refName (the OpenAPI default).
  • Member side: inline : Pet supertype clause rendered by the parameter-list value (after the parens), and the discriminator property OMITTED — filtered BEFORE the property walk, or its enum schema synthesizes a spurious sibling. Same package by the export-path policy satisfies Kotlin's sealed same-package rule.
  • Jackson vs kotlinx flavor: tags are parent-side (@JsonSubTypes), so members carry no tag annotation and one member may hold different tags under different parents (the kotlinx one-@SerialName-per-class conflict rule does not apply).
  • Runtime gotcha (probed): a raw writeValueAsString(list) erases the element type and silently DROPS the tags; concrete roots, writerFor(type), and full-generic types all write them — Spring MVC uses the typed path, so real consumers are fine. Test round-trips with a typed writer.
  • allOf-composed members (the spec's canonical idiom: shared fields on a base, members compose via allOf) qualify WITHOUT special handling — core resolves allOf at parse time (mergeIntersection), so the member peeks as a flat object and the base's fields flatten into each data class (verified through the pipeline 2026-08-04). Flattening is the right Kotlin target: the sealed interface is the polymorphism seam, not class inheritance.
  • Inline unions (stage 2, shipped): a qualifying union ANYWHERE — component property, operation body/response/header/parameter — synthesizes its sealed parent under its stackTrail name (combinator frames oneOf/anyOf/allOf are structural and elided; a parameters/<index> position resolves to the parameter NAME via a WeakMap document scan — the trail itself cannot carry names, it doubles as a JSON Pointer where parameters is an array) into the MODELS package (toModelExportPath — ONE placement policy for EVERY synthesized declaration; caller's-file placement breaks 'reuse'-across-files for cross-package peers). The scan deep-walks components AND operations AND webhooks (headers and the content alternative included); synthesized claims carry the union NODE so ensureSealedParent lets WHOEVER needs the name first declare it via the claim registry. Derivability is ONE shared non-throwing probe (toSynthesizedNameOrNull) across scan/render/members — underivable roots degrade consistently to pre-synthesis behavior; the object/enum sites deliberately keep the THROWING derivation (no honest fallback exists for structure). One member may implement several sealed parents (parent-side tags).
  • Not yet built: undiscriminated unions (stage 3 — enrichment-asserted hints / Jackson Id.DEDUCTION) and the INVERTED swagger-style pattern (discriminator on the base, no oneOf, membership implied by allOf back-references — no union node exists, so no sealed interface).

9. Kotlin pitfalls

Symptom Fix
@Serializable/KDoc missing Mirror annotations/description getters on the projection
Annotation silently dropped Real KtAnnotation instances, not strings
segment 'x' is not a valid package name part Fix the export-path policy — packages validate, never sanitize
Import mid-file / duplicated register / annotation packageName, never templates
String?? Type expression owns the single ?
Unknown Kotlin entity type Use the seven Kotlin factories, not TS kinds
Empty data class throws Shape dispatch must route empty objects to typealias
TDZ crash at module load Break base↔router↔projection cycles with a leaf module (peekSchema pattern)
Nondeterministic output No module state; config via enrichments; memoize document scans in WeakMap
data class X glued to Map<String, Any?> The normalized-insert type/declaration gap — §8b, don't hack around it
Union renders Any/JsonNode where a sealed type was expected Qualifying predicate failed — check discriminator presence, all-ref members, per-member surviving parameters (§8c)
Member missing : Parent / spurious discriminator enum sibling Membership scan not consulted before construction, or omission applied after the property walk (§8c)
Sealed round-trip loses the wire tag at runtime Jackson root-list type erasure — serialize via a typed writer; generated code is correct (§8c)

Appendix — generated API reference

The full deno doc surface for the packages this skill covers lives in appendix.md, in this skill's directory — generated from framework source — signatures and field docs only. It is authoritative: when the prose above does not carry the exact constructor or field shape you need, Read (or grep) appendix.md instead of diving into package source. Do not guess signatures. For a symbol not listed there, deno doc <file> <Symbol> against the framework source beats grepping it.

Files (skmtc)
  • appendix.md 37.6 KB
    # Appendix — generated API reference
    
    > Generated from framework source by
    > `deno run --allow-read --allow-write --allow-env --allow-run=deno,git .scripts/generate-skill-api-appendix.ts`
    > (from `deno/`). **Authoritative** for signatures, fields, and doc
    > comments — trust it instead of re-reading package source. JSDoc
    > `@example` blocks are stripped at generation. For a symbol not
    > listed here, `deno doc <file> <Symbol>` against the framework
    > source beats grepping it.
    
    ### `@skmtc/lang-kotlin` — the full exported surface
    
    Every export of the package, with exact constructor/argument shapes. The prose sections above explain how the pieces compose; this is the complete signature-level truth.
    
    ### `lang-kotlin/mod.ts`
    
    ```text
    @module
        @skmtc/lang-kotlin
    
        The Kotlin target-language layer for SKMTC generators.
    
        Status: production (Phase D + the Kotlin milestone arc complete).
        The full register/write path on the frozen language seam: the
        `kotlin` {@link Lang} object,
        `KtSnippet` (static `lang`, keyless registers), the register family
        (`register`/`defineAndRegister` + `KtRegisterArgs` — deliberately no
        `reExports` field), the projection-base veneers (model + OAS
        operation), `KtFile` (path-derived `package` directive, sorted imports,
        same-package suppression), `KtImport` (symbol-level, `as` aliases),
        `KtDefinition` (head + value rendering — the identifier renders its
        declaration head, the value renders everything after it via
        `KtParameterList` / `KtPrimaryConstructor` plus inline supertype
        clauses and ` {\n…\n}` bodies; the `KtAnnotated` / `KtDocumented`
        value protocols cover what renders above the declaration), the
        function-signature grammar (`KtFunctionSignature` /
        `KtFunctionParameter` — interface/class methods incl. KDoc,
        expression bodies, and parameter defaults), the identifier
        factories, `sanitizePropertyName`
        (hard keywords + backticks), and `toPackageName` (segment-validated).
    
        Grammar only: serialization flavor (kotlinx annotations) is generator
        policy — `@skmtc/gen-kotlin` is the proving generator. Architecture
        spec: `notes/lang/19-kotlin-architecture.md`. Template:
        `@skmtc/lang-typescript`.
    
    Defined in deno/lang-kotlin/src/createIdentifier.ts:84:14
    
    function createClass(name: string, args: CreateKtIdentifierArgs): KtIdentifier
      Creates a concrete `class` identifier.
    
    Defined in deno/lang-kotlin/src/createIdentifier.ts:97:14
    
    function createDataClass(name: string, args: CreateKtIdentifierArgs): KtIdentifier
      Creates a `data class` identifier.
    
    Defined in deno/lang-kotlin/src/createIdentifier.ts:110:14
    
    function createEnumClass(name: string, args: CreateKtIdentifierArgs): KtIdentifier
      Creates an `enum class` identifier.
    
    Defined in deno/lang-kotlin/src/createIdentifier.ts:123:14
    
    function createInterface(name: string, args: CreateKtIdentifierArgs): KtIdentifier
      Creates an `interface` identifier.
    
    Defined in deno/lang-kotlin/src/createIdentifier.ts:136:14
    
    function createSealedInterface(name: string, args: CreateKtIdentifierArgs): KtIdentifier
      Creates a `sealed interface` identifier.
    
    Defined in deno/lang-kotlin/src/createIdentifier.ts:152:14
    
    function createTypeAlias(name: string, args: CreateKtIdentifierArgs): KtIdentifier
      Creates a `typealias` identifier.
    
    Defined in deno/lang-kotlin/src/createIdentifier.ts:172:14
    
    function createValue(name: string, args: CreateValueArgs): KtIdentifier
      Creates a top-level `val` identifier — Kotlin's distinctive file-scope
      value.
    
    Defined in deno/lang-kotlin/src/register.ts:102:14
    
    function defineAndRegister<Value extends GeneratedValue>(context: GenerateContextType, {identifier, value, destinationPath, description}: KtDefineAndRegisterArgs<Value>): KtDefinition<Value>
      Build a {@link KtDefinition} from `value` and register it at
      `destinationPath`. The transform-level counterpart of
      `this.defineAndRegister` — a transform (a closure with no class) imports
      this directly; the language comes from the import, like everything else.
    
      No cache check — callers wrap with `context.findDefinition` first where
      dedup is wanted (the gen-msw accumulator pattern).
    
    Defined in deno/lang-kotlin/src/KtDocumented.ts:19:14
    
    function isKtDocumented(value: unknown): value is KtDocumented
      Type guard for the {@link KtDocumented} protocol — narrows without casts.
    
    Defined in deno/lang-kotlin/src/createIdentifier.ts:53:14
    
    function isKtEntityType(type: string): type is KtEntityType
      Type guard — whether an opaque `type` string is one this language knows.
    
    Defined in deno/lang-kotlin/src/KtIdentifier.ts:83:14
    
    function isKtIdentifier(identifier: IdentifierBase): identifier is KtIdentifier
      Type guard narrowing a neutral {@link IdentifierBase} to a
      {@link KtIdentifier} — the cast-free way the renderer reads `type`.
    
    Defined in deno/lang-kotlin/src/hardKeywords.ts:54:14
    
    function isKtIdentifierName(name: string): boolean
      Whether `name` is a plain (unescaped) Kotlin identifier: a letter or
      underscore followed by letters, digits, or underscores. Deliberately
      ASCII-conservative — Kotlin permits unicode letters, but anything
      outside ASCII gets the backtick treatment from
      {@link import('./sanitizePropertyName.ts').sanitizePropertyName},
      which is always safe.
    
      Note this is a SYNTAX check only — a hard keyword like `object`
      matches the regex but still needs escaping. Callers check
      {@link ktHardKeywords} separately.
    
    Defined in deno/lang-kotlin/src/register.ts:49:14
    
    function register(context: GenerateContextType, args: KtRegisterArgs & { destinationPath: string; }): void
      Kotlin's register function — the single implementation behind
      {@link KtSnippet.register} and the projection-base veneers.
    
      Converts the concise import form into {@link KtImport} objects, creates
      the destination {@link KtFile} on first write (caller-side creation —
      the language is right here), and hands pure data to the neutral
      `context.register`. No `generatorId`, no `Lang` object: the language is
      this module. Throws when the destination file exists but was created by
      another language — a cross-language collision is a misconfiguration,
      refused loudly rather than mixing Kotlin content into a foreign file.
    
    Defined in deno/lang-kotlin/src/sanitizePropertyName.ts:27:14
    
    function sanitizePropertyName(propertyName: string): string
      Makes a property name safe as a Kotlin declaration name.
    
      - A plain identifier that is not a hard keyword → returned as-is.
      - A hard keyword (`object`, `val`, …) or a syntactically invalid name
        (`user name`, `1st`) → backtick-escaped (``object``).
      - A name that backticks cannot save (contains `.`, `;`, `:`, `/`,
        `\`, `[`, `]`, `<`, `>`, a backtick, or a newline — illegal on the
        JVM even escaped) → throws. Generators camelCase wire names before
        calling this, so reaching the throw means a naming policy bug, not
        a schema problem.
    
      Renames are deliberately NOT this function's job: wire-name mismatches
      are handled gen-side via serialization annotations (`@SerialName`);
      this function only guarantees the chosen name parses. The two compose
      — a backticked keyword (``object``) still equals its wire name, so
      it needs no annotation.
    
      Returns a plain `string` (unlike the TypeScript version's key-value
      fallback — Kotlin has no quoted-property syntax to fall back to).
    
    Defined in deno/lang-kotlin/src/KtAnnotation.ts:149:14
    
    function toKtAnnotations(value: unknown): KtAnnotations
      Collect a value's {@link KtAnnotated} protocol field into a
      {@link KtAnnotations} block — empty when the value carries none, so the
      caller renders it without a guard.
    
    Defined in deno/lang-kotlin/src/createIdentifier.ts:188:14
    
    function toKtEntityType(type: string): KtEntityType
      Narrow the engine's opaque `type: string` (from `Lang.toIdentifier`'s
      neutral args) to this language's {@link KtEntityType} — cast-free, via
      {@link isKtEntityType}. Throws on a type outside the vocabulary, a loud
      signal that an identifier built for another language (or with a typo'd
      type) reached the Kotlin renderer. (Unlike TypeScript there is no
      keyword map here — the declaration keywords live on
      {@link import('./KtIdentifier.ts').KtIdentifier}'s declaration-head
      render, the only place they are used.)
    
    Defined in deno/lang-kotlin/src/toKtModelProjectionBase.ts:32:14
    
    function toKtModelProjectionBase<EnrichmentType = undefined>(config: ModelProjectionBaseConfig<EnrichmentType, KtIdentifierType>)
      Build a Kotlin model projection base class.
    
      Thin veneer over core's `toModelProjectionBase`: passes `KtSnippet` as the
      base (the hierarchy is language-bound at its root) and adds the register
      ergonomics core deliberately doesn't define — typed with Kotlin's concise
      vocabulary, which core can't name:
    
      - `register(args)` — own-file: `destinationPath` is always this
        projection's `settings.exportPath` (the foundation rule; never a
        fallback).
      - `registerInto(destinationPath, args)` — the explicit cross-file path.
    
      Both delegate to this package's register function — never
      `super.register` (lang-base members are type-erased on core's factory
      result).
    
      The config is core's `ModelProjectionBaseConfig` parameterized over
      {@link KtIdentifierType} (so `toIdentifierType` returns the `type` bound to
      `KtEntityType`). The base is the factory's first argument, not a config
      field.
    
      The companion operation veneer {@link toKtOasOperationProjectionBase} has
      arrived (the OAS veneer now exists, driven by gen-kotlin-sdk's Response
      models).
    
    Defined in deno/lang-kotlin/src/toKtOasOperationProjectionBase.ts:31:14
    
    function toKtOasOperationProjectionBase<EnrichmentType = undefined>(config: OasOperationProjectionBaseConfig<EnrichmentType, KtIdentifierType>)
      Build a Kotlin OAS operation projection base class — the first
      operation-keyed Kotlin projection family (demanded by
      gen-kotlin-sdk's Response models, arc note `32` §C4; earlier
      operation generators were accumulator-style and didn't need one).
    
      Thin veneer over core's `toOasOperationProjectionBase`: passes `KtSnippet`
      as the base (the hierarchy is language-bound at its root) and adds the
      register ergonomics core deliberately doesn't define — typed with Kotlin's
      concise vocabulary, which core can't name:
    
      - `register(args)` — own-file: `destinationPath` is always this
        projection's `settings.exportPath` (the foundation rule; never a
        fallback).
      - `registerInto(destinationPath, args)` — the explicit cross-file path.
    
      Both delegate to this package's register function — never
      `super.register` (lang-base members are type-erased on core's factory
      result).
    
      The config is core's `OasOperationProjectionBaseConfig` parameterized over
      {@link KtIdentifierType} (so `toIdentifierType` returns the `type` bound to
      `KtEntityType`). The base is the factory's first argument, not a config
      field.
    
    Defined in deno/lang-kotlin/src/toPackageName.ts:27:14
    
    function toPackageName(path: string, packages?: ModulePackage[]): string
      Derives the `package` directive from a Kotlin file's export path —
      the segments after the `@/` root ARE the package directories
      (Kotlin's package-=-folder convention; `client.json#settings.basePath`
      points at the Gradle source root, e.g. `./app/src/main/kotlin`).
    
      - `@/com/example/api/User.generated.kt` → `'com.example.api'`
      - `@/User.kt` → `''` (the default package — legal, discouraged;
        {@link import('./KtFile.ts').KtFile} renders no `package` line)
    
      Multi-package output (`client.json#settings.packages`): export paths
      are forward paths under a package's `rootPath`
      (`my-sdk-core/src/main/kotlin/com/example/User.kt`), and the package
      directories are the segments after the OWNING package's `rootPath` —
      pass `packages` and the longest matching `rootPath` prefix is
      stripped before derivation. Each `rootPath` is that module's Gradle
      source root, exactly as `basePath` is in single-package mode.
    
      Throws when any directory segment is not a plain Kotlin identifier or
      is a hard keyword — a generator authored a path that cannot be a
      package (`@/my-models/User.kt`). Loud beats backticked package names.
      This is Kotlin's `validateDestinationPath`.
    
    Defined in deno/lang-kotlin/src/withDescription.ts:20:14
    
    function withDescription(value: Stringable, {description}: WithDescriptionArgs): string
      Wraps a value with a KDoc comment when a description is provided —
      Kotlin's block-comment syntax is identical to JSDoc, so this mirrors
      the lang-typescript helper.
    
      A multi-line description renders as a block with `*` margins —
      the inline form would leave continuation lines without a comment
      margin, so a formatter eats a content-leading `*` as decoration and
      intra-line indentation is lost.
    
    Defined in deno/lang-kotlin/mod.ts:34:14
    
    const fileExtensions: ".kt"[]
      File extensions this language package renders.
    
    Defined in deno/lang-kotlin/src/KtLang.ts:16:14
    
    const kotlin: Lang
      The Kotlin {@link Lang} — carried as the static `lang` on
      {@link import('./KtSnippet.ts').KtSnippet} and inherited by every class
      built on it. Its only consumers are the engine's Drivers, which read it
      off the projection class (`projection.lang`) ephemerally at each use
      site. The engine reaches Kotlin only through these neutral factories;
      it never names `KtFile` / `KtDefinition` / `KtImport` itself.
    
    Defined in deno/lang-kotlin/src/hardKeywords.ts:9:14
    
    const ktHardKeywords: ReadonlySet<string>
      Kotlin's hard keywords — names that can never be used as identifiers
      without backtick escaping. Soft keywords (`value`, `data`, `field`,
      `import`, …) and modifier keywords (`sealed`, `internal`, …) are NOT
      in this set: they are legal identifiers in Kotlin and need no escape.
    
      Source: the Kotlin language spec's "hard keywords" list (pinned in
      `notes/lang/19-kotlin-architecture.md`).
    
    Defined in deno/lang-kotlin/mod.ts:31:14
    
    const langId: "kotlin"
      The language id this package targets.
    
    Defined in deno/lang-kotlin/src/KtAnnotation.ts:72:1
    
    class KtAnnotation
      Renders a Kotlin annotation: `@Serializable`, `@SerialName("user_id")`.
    
      A registering LEAF entity (the `TsHeritage` precedent): given a
      `packageName` it registers its own class's import into
      `destinationPath`, so the annotation and its import are one statement
      that cannot drift apart. It registers unconditionally — a same-package
      annotation's import is dropped centrally by `KtFile`'s render-time
      suppression, so callers need no such check. Container renderers
      ({@link KtAnnotations}, `KtParameterList`, `KtFunctionSignature`) stay
      pure and just interpolate.
    
      NOT a `KtSnippet` subclass: `KtDefinition` imports {@link toKtAnnotations}
      from this module, so extending `KtSnippet` would close a load-time module
      cycle (`KtSnippet → KtLang → KtDefinition → KtAnnotation → KtSnippet`).
      It calls this package's {@link register} function directly instead — the
      same write path `KtSnippet.register` delegates to.
    
      Generic grammar only — args are {@link Stringable} and pre-quoted by the
      caller. WHICH annotation to emit is generator policy (the serialization
      seam lives in `gen-kotlin`); this package only renders what it is handed.
    
      constructor({context, name, args, target, packageName, destinationPath}: KtAnnotationArgs)
      name: string
      args: Stringable[]
      target: KtAnnotationTarget | undefined
      toString(): string
    
    Defined in deno/lang-kotlin/src/KtAnnotation.ts:132:1
    
    class KtAnnotations
      A class-level annotation block — zero or more {@link KtAnnotation}s,
      rendered one per line above a declaration head. Empty renders the empty
      string, so it interpolates unconditionally
      (`${annotations}${head}${value}`).
    
      constructor(annotations: KtAnnotation[])
      annotations: KtAnnotation[]
      toString(): string
    
    Defined in deno/lang-kotlin/src/KtDefinition.ts:57:1
    
    class KtDefinition<Value extends GeneratedValue = GeneratedValue> extends DefinitionBase<Value>
      Kotlin's concrete {@link DefinitionBase}: renders the identifier's
      declaration head and the value, each rendering itself.
    
      - Assignment kinds (`typealias`, `val`) — `${head} = ${value}`;
        the value is the right-hand-side expression.
      - Declaration kinds (`class`, `data-class`, `enum-class`,
        `interface`, `sealed-interface`) — `${head}${value}`; the value
        renders everything after the head: a
        {@link import('./KtParameterList.ts').KtParameterList} (parentheses
        included), a
        {@link import('./KtPrimaryConstructor.ts').KtPrimaryConstructor}
        (modifiers + the explicit `constructor` keyword), plus inline
        ` : A, B` supertype clauses and ` {\n…\n}` braced bodies — plain
        Kotlin syntax carries no grammar rule worth a class. A value that
        renders nothing yields the bodyless idiom (`sealed interface Animal`, `class Marker`) — the value decides its own form; the
        definition never inspects it.
        (Raw whole-file content — static template files — is a FILE fact, not
        a definition: it flows through the register vocabulary's `custom`
        field onto `FileBase.custom`, with no identifier involved.)
    
      Two protocols remain on the value because they render OUTSIDE the
      head+value line: class-level annotations
      ({@link import('./KtAnnotation.ts').KtAnnotated}, one per line above
      the declaration — the neutral `Lang.toDefinition` signature has no
      annotations slot) and KDoc
      ({@link import('./KtDocumented.ts').KtDocumented}, above the
      annotations; an explicit constructor `description` wins).
    
      (A foreign identifier is refused earlier, at the `Lang.toDefinition`
      boundary in `KtLang`; the constructor only accepts a
      {@link KtIdentifier}.)
    
      Visibility is the identifier's fact, rendered in its head (`private data class …` — see {@link KtIdentifier.toString}). The neutral
      `noExport` flag the Drivers pass is folded into a restricted identifier
      copy at the `KtLang.toDefinition` boundary, so this class never sees
      it.
    
      constructor({context, identifier, value, description}: KtDefinitionArgs<Value>)
      identifier: KtIdentifier
        Narrows the inherited neutral `identifier` to the concrete Kotlin
        subclass (the constructor only accepts a {@link KtIdentifier}).
      description: string | undefined
      override toString(): string
    
    Defined in deno/lang-kotlin/src/KtFile.ts:43:1
    
    class KtFile extends CodeFileBase
      Kotlin's concrete code file. Owns the definition + import collections and
      their merge policy (the neutral {@link CodeFileBase} declares the
      contract) and adds the Kotlin-specific pieces:
    
      - the `package` directive, DERIVED from the file's own path via
        {@link toPackageName} — the export path encodes the package
        (`@/com/example/api/User.generated.kt` → `package com.example.api`);
        `client.json#settings.basePath` points at the Gradle source root.
      - same-package import suppression: any import whose resolved
        package equals this file's package is omitted at render (same-package
        symbols need no import in Kotlin — the structural analog of TsFile's
        intra-package `@/` normalization). In particular the Driver's
        cross-file peer imports vanish when peers share the package.
      - the rendering arrangement: the neutral `custom` slot
        ({@link FileBase.custom}) first — leading content above the
        `package` directive (e.g. a generated-file attribution banner;
        only comments may precede `package`), the same placement `TsFile`
        gives it — then the package directive, imports (one statement
        per symbol, sorted alphabetically — not style, which is the
        consumer's formatter's job, but registration-order independence:
        the rendered bytes are what snapshot tests and byte-identical
        regression gates compare), then definitions joined by blank lines.
    
      `reExports` cannot arrive by construction — Kotlin's concise register
      vocabulary has no `reExports` field and the Driver never registers
      them — so rendering ignores the (always empty) neutral map.
    
      constructor({path, settings}: KtFileArgs)
      packageName: string
        The `package` this file declares — derived from `path`, with the
        owning package's `rootPath` stripped first in multi-package mode
        (`settings.packages`).
      settings: ClientSettings | undefined
        Threaded into package derivation and same-package suppression.
      definitions: Map<string, DefinitionBase>
        Definitions keyed by identifier name (first write wins; Kotlin has no declaration merging).
      imports: Map<string, ImportBase>
        Imports keyed by {@link ImportBase.mergeKey}.
      reExports: Map<string, ReExportBase>
        Re-exports keyed by {@link ReExportBase.mergeKey} — Kotlin registers none; kept for the neutral contract.
      override addDefinition(definition: DefinitionBase): void
      override addImports(incoming: ImportBase[]): void
      override addReExports(incoming: ReExportBase[]): void
      override findDefinitions(query?: { name?: string; type?: KtEntityType; }): DefinitionBase[] | undefined
      override toString(): string
    
    Defined in deno/lang-kotlin/src/KtFunctionSignature.ts:30:1
    
    class KtFunctionParameter
      Renders a Kotlin function parameter: `@PathVariable("id") id: String`,
      `verbose: Boolean?`.
    
      Grammar only — WHICH annotations to attach (`@PathVariable`,
      `@RequestParam`, `@RequestBody`) is generator policy riding
      {@link import('./KtAnnotation.ts').KtAnnotation}. Distinct from
      {@link import('./KtParameterList.ts').KtParameterArgs} (primary-constructor
      parameters, `val` prefix + defaults) — the two are different productions.
    
      constructor({name, type, nullable, defaultValue, annotations}: KtFunctionParameterArgs)
      name: string
      type: Stringable
      nullable: boolean | undefined
      defaultValue: Stringable | undefined
      annotations: KtAnnotation[] | undefined
      toString(): string
    
    Defined in deno/lang-kotlin/src/KtFunctionSignature.ts:93:1
    
    class KtFunctionSignature
      Renders a Kotlin method signature — the building block of an
      `interface` or `class` body:
    
      ```kotlin
          @GetMapping("/users/{id}")
          fun getUsersId(@PathVariable("id") id: String, @RequestParam("verbose") verbose: Boolean?): User
      ```
    
      Indented one level (it lives inside a declaration body); parameters on
      one line (formatting is the consumer's formatter's job). Abstract by
      default; an expression `body` renders the delegation form (` = …` —
      block bodies deliberately unsupported). Optional KDoc `description`
      above the annotations and per-parameter `= default`. Grammar only —
      no `suspend`; the mapping annotations are generator policy.
    
      constructor({name, parameters, returnType, annotations, description, body}: KtFunctionSignatureArgs)
      name: string
      parameters: KtFunctionParameter[]
      returnType: Stringable | undefined
      annotations: KtAnnotation[] | undefined
      description: string | undefined
      body: Stringable | undefined
      toString(): string
    
    Defined in deno/lang-kotlin/src/KtIdentifier.ts:50:1
    
    class KtIdentifier extends IdentifierBase
      Kotlin's concrete {@link IdentifierBase}: adds the typed `type`
      ({@link KtEntityType}) and owns the rendering of its own declaration
      head — `data class User`, `enum class Status`, `val timeout: Long` —
      via {@link toString}. {@link import('./KtDefinition.ts').KtDefinition}
      interpolates the head and adds only the kind's arrangement (parameter
      parens, supertype clause, braced body); the keyword itself lives here,
      next to the identifier that determines it.
    
      The engine holds it as the neutral `IdentifierBase` (reading only
      `.name`); `KtDefinition` narrows back to `KtIdentifier` via
      {@link isKtIdentifier} to read `type`.
    
      constructor({name, typeName, exported, type}: KtIdentifierArgs)
      type: KtEntityType
        Per-language declaration type — drives the declaration head and shell.
      override toString(): string
        The declaration head: `[private ]<keyword> <name>[: <typeName>]`.
        Overrides the neutral base's bare-name render — in Kotlin the keyword
        belongs to the identifier's kind, so the identifier renders it, and
        visibility is the identifier's own `exported` fact (the pattern core's
        `IdentifierBase.exported` doc anticipates: each language renders it its
        own way — Go via name casing, Kotlin via this prefix). Kotlin defaults
        to `public`, so `exported` renders as nothing when true and
        `private ` (file-local) when false — keyword only to restrict.
        Generators splicing a name into generated code should keep using
        `.name` / `Inserted.toName()`, which this override does not touch.
    
    Defined in deno/lang-kotlin/src/KtImport.ts:46:1
    
    class KtImport extends ImportBase
      Kotlin's concrete {@link ImportBase}: one module's worth of imported
      symbols. The `module` takes two forms, distinguished by shape:
    
      - a dotted package (`'kotlinx.serialization'`) — external libraries,
        generator-registered;
      - an `@/`-export-path (`'@/com/example/api/User.generated.kt'`) — project
        files; this is what the Driver passes for cross-file peer imports.
    
      The path form resolves to its package via {@link toPackageName} at
      render time ({@link resolvedPackage}); {@link import('./KtFile.ts').KtFile}
      uses the same resolution to suppress same-package imports (same-package
      symbols need no import in Kotlin).
    
      Rendering is one statement per symbol — Kotlin has no brace grouping:
      `import kotlinx.serialization.Serializable`.
    
      constructor(module: string, specifiers: KtImportSpecifier[])
      module: string
      specifiers: KtImportSpecifier[]
      static fromConcise(module: string, names: KtImportNameArg[]): KtImport
        Build from the concise `{ module: KtImportNameArg[] }` form a generator passes.
      static fromIdentifier(module: string, identifier: IdentifierBase): KtImport
        Build the import of a single {@link IdentifierBase} from `module` — the
        cross-file import a Driver registers when a generator references a
        peer's Definition. The identifier's `type` is ignored: every Kotlin
        import has the same form, so the neutral `IdentifierBase` (which the
        engine holds) is all that's needed — no narrowing.
      resolvedPackage(packages?: ModulePackage[]): string
        The package this import's symbols come from: a path-form module
        (contains `/`) derives via {@link toPackageName}; a dotted package
        passes through. In multi-package output the owning
        {@link import('./KtFile.ts').KtFile} passes its `settings.packages`
        so a path under another module's `rootPath` resolves to that
        module's real dotted package — Kotlin imports are always packages;
        `moduleName` has no Kotlin meaning.
      override mergeKey(): string
      override merge(other: ImportBase): ImportBase
      toLines(packages?: ModulePackage[]): string[]
        One `import pkg.Name[ as Alias]` line per specifier.
      override toString(): string
        The packages-less fallback render — correct for dotted-package modules
        and for path-form modules in single-package projects. The canonical
        render path is {@link import('./KtFile.ts').KtFile}'s `toString`, which
        calls {@link toLines} with the project's `settings.packages` so a
        path-form module under another module's `rootPath` resolves to that
        module's real package; the neutral `ImportBase` signature gives this
        override no way to receive them, so multi-package resolution is only
        correct through `KtFile`.
    
    Defined in deno/lang-kotlin/src/KtParameterList.ts:38:1
    
    class KtParameterList
      Renders a Kotlin primary-constructor parameter list, parentheses
      included — `(\n    val id: String\n)`. The value owns its
      delimiters: a data-class value interpolates this directly
      (`${parameters}${supertypeClause}`), and the definition renders only
      `${head}${value}`. Each parameter is a `val` property (public by
      default).
    
      No trailing comma after the last parameter — a cosmetic non-decision:
      trailing commas are the consumer's formatter's territory (ktfmt adds
      them, ktlint can enforce either way) and SKMTC renders unformatted by
      design.
    
      constructor(parameters: KtParameterArgs[])
      parameters: KtParameterArgs[]
      toString(): string
    
    Defined in deno/lang-kotlin/src/KtPrimaryConstructor.ts:32:1
    
    class KtPrimaryConstructor
      A primary constructor — the clause after the class name, owned by the
      VALUE: a class value composes
      `${primaryConstructor}${supertypeClause}${body}` and the definition
      renders `${head}${value}`. Without modifiers this is just the
      parameter list; the class exists for the modifier + explicit
      `constructor`-keyword grammar rule.
    
      constructor({parameters, modifiers}: KtPrimaryConstructorArgs)
      parameters: Stringable
      modifiers: Stringable | undefined
      toString(): string
    
    Defined in deno/lang-kotlin/src/KtSnippet.ts:29:1
    
    class KtSnippet extends SnippetBase
      The Kotlin snippet base — where the Kotlin language enters the SKMTC
      DSL class hierarchy.
    
      `@skmtc/core`'s {@link SnippetBase} is language-blind and needs no
      `generatorKey` to register — the key stays an optional constructor arg
      used for attribution (gen-maps) only. `KtSnippet` extends it and carries
      the Kotlin {@link Lang} as a static only: Drivers read it off the
      projection class (`projection.lang`), pre-construction, inherited through
      every class built on this base (including projection classes from this
      package's `toKtModelProjectionBase`). No instance slot — the register
      methods delegate to this package's register functions, which name the
      Kotlin classes directly.
    
      `destinationPath` is always explicit on snippets: a snippet has no
      file or settings of its own, so the parent passes the target path through
      the constructor. Own-file defaulting exists only on projections, in the
      projection-base veneers.
    
      static lang: Lang
        The language every class built on this base renders into — the neutral
        {@link Lang}. Drivers read it off the projection class
        (`projection.lang`) pre-construction; a projection-base veneer carries
        the Kotlin identifier tightening ({@link import('./KtIdentifier.ts').KtIdentifierType})
        through its config's `toIdentifierType` rather than through this static.
      register(args: KtRegisterArgs & { destinationPath: string; }): void
        Register imports / definitions into the file at `destinationPath`,
        typed by Kotlin's concise vocabulary — keyless: no `generatorId`
        resolution, no `generatorKey` requirement.
      defineAndRegister(args: KtDefineAndRegisterArgs<Value>): KtDefinition<Value>
        Build a {@link KtDefinition} from `value` and register it at
        `destinationPath`.
    
    Defined in deno/lang-kotlin/src/createIdentifier.ts:59:1
    
    type CreateKtIdentifierArgs = { exported?: boolean; }
      Options shared by the identifier factories — every field optional, so
      the common case stays `createDataClass(name)`.
    
    Defined in deno/lang-kotlin/src/createIdentifier.ts:68:1
    
    type CreateValueArgs = { typeName?: string; exported?: boolean; }
      Options for {@link createValue} — the only factory with a `typeName`
      slot (the `val x: T = …` annotation).
    
    Defined in deno/lang-kotlin/src/KtAnnotation.ts:107:1
    
    type KtAnnotated = { annotations: KtAnnotation[]; }
      The protocol by which a Definition's VALUE supplies class-level
      annotations to {@link import('./KtDefinition.ts').KtDefinition}.
    
      `Lang.toDefinition`'s neutral signature has no annotations slot, so
      annotations ride on the value: a generator's projection sets an
      `annotations` field, and `KtDefinition.toString()` collects it via
      {@link toKtAnnotations} and renders the annotations above the
      declaration head.
    
    Defined in deno/lang-kotlin/src/KtAnnotation.ts:23:1
    
    type KtAnnotationArgs = { context: GenerateContextType; name: string; args?: Stringable[]; target?: KtAnnotationTarget; packageName?: string; destinationPath: string; }
      Constructor arguments for {@link KtAnnotation}.
    
    Defined in deno/lang-kotlin/src/KtAnnotation.ts:8:1
    
    type KtAnnotationTarget = "field" | "get" | "set" | "param" | "property" | "receiver" | "setparam" | "delegate" | "file" | "all"
      Kotlin's annotation use-site targets — the `field:` in
      `@field:JsonAnySetter`. Grammar-level, so the set is closed and owned
      here; WHICH target a generator picks is its policy.
    
    Defined in deno/lang-kotlin/src/register.ts:77:1
    
    type KtDefineAndRegisterArgs<Value extends GeneratedValue> = { identifier: KtIdentifier; value: Value; destinationPath: string; description?: string; }
      Arguments for {@link defineAndRegister}.
    
    Defined in deno/lang-kotlin/src/KtDefinition.ts:10:1
    
    type KtDefinitionArgs<Value extends GeneratedValue> = { context: GenerateContextType; identifier: KtIdentifier; value: Value; description?: string; }
      Constructor arguments for {@link KtDefinition}.
    
    Defined in deno/lang-kotlin/src/KtDocumented.ts:12:1
    
    type KtDocumented = { description?: string; }
      The protocol by which a Definition's VALUE supplies a KDoc description
      to {@link import('./KtDefinition.ts').KtDefinition} — a value-carried
      protocol (like `KtAnnotated`) because it renders ABOVE the head+value
      line and the neutral `Lang.toDefinition` call the Drivers make carries
      no description; threading it through core would change every
      language's output at once. The lang renders the KDoc; WHAT the text is
      (a schema `description`, an operation `summary`) is generator policy.
    
      An explicit `description` passed to `KtDefinition`'s constructor wins
      over the protocol.
    
    Defined in deno/lang-kotlin/src/createIdentifier.ts:32:1
    
    type KtEntityType = (typeof ktEntityTypes)[number]
      Kotlin's declaration-type vocabulary — the typed `type` this package
      writes onto its {@link KtIdentifier} and the discriminator its renderers
      narrow against.
    
      - `'class'` — a concrete `class Name(…) { … }` declaration (the
        generated-controller idiom; the value composes its
        `KtPrimaryConstructor` and braced body).
      - `'data-class'` — a `data class Name(…)` DTO container.
      - `'enum-class'` — an `enum class Name { … }` declaration.
      - `'interface'` — an `interface Name { … }` declaration (the Spring
        "interfaceOnly" idiom — abstract method signatures the consumer
        implements).
      - `'sealed-interface'` — a `sealed interface Name` (the `oneOf` idiom).
      - `'typealias'` — a `typealias Name = …` declaration.
      - `'val'` — a top-level `val Name = …` assignment (Kotlin's distinctive
        file-scope value, illegal in C#/PHP/Java).
    
      Every kind names a REAL declaration — an identifier that never appears
      in code is a contradiction. Raw whole-file content (static template
      files) is a FILE fact, not a definition: it goes through the register
      vocabulary's `custom` field (`FileBase.custom`), with no identifier
      involved.
    
      Unlike TypeScript, the type does NOT drive import form — every Kotlin
      import is `import pkg.Name`. It drives only the declaration shell.
      Deferred kinds (`object`, `fun`, `var`, `const-val`) arrive with the
      milestones that need them; {@link toKtEntityType} throwing on them is
      the desired behavior until then.
    
    Defined in deno/lang-kotlin/src/KtFile.ts:10:1
    
    type KtFileArgs = { path: string; settings: ClientSettings | undefined; }
      Constructor arguments for {@link KtFile} — the `Lang.createFile` shape.
    
    Defined in deno/lang-kotlin/src/KtFunctionSignature.ts:4:1
    
    type KtFunctionParameterArgs = { name: string; type: Stringable; nullable?: boolean; defaultValue?: Stringable; annotations?: KtAnnotation[]; }
      A single parameter of a Kotlin function signature.
    
    Defined in deno/lang-kotlin/src/KtFunctionSignature.ts:59:1
    
    type KtFunctionSignatureArgs = { name: string; parameters: KtFunctionParameterArgs[]; returnType?: Stringable; annotations?: KtAnnotation[]; description?: string; body?: Stringable; }
      Constructor arguments for {@link KtFunctionSignature}.
    
    Defined in deno/lang-kotlin/src/KtIdentifier.ts:18:1
    
    type KtIdentifierArgs = IdentifierBaseArgs & { type: KtEntityType; }
      Constructor arguments for {@link KtIdentifier} — the neutral
      {@link IdentifierBaseArgs} plus this language's typed `type`.
    
    Defined in deno/lang-kotlin/src/KtIdentifier.ts:12:1
    
    type KtIdentifierType = IdentifierType & { type: KtEntityType; }
      The non-`name` parts of a Kotlin identifier — the tightened
      `IdentifierType` a Kotlin projection's `toIdentifierType` returns.
      Core's neutral {@link IdentifierType} carries an opaque `type: string`;
      this alias narrows it to {@link KtEntityType}, the named form generators
      annotate with. The engine spreads it into
      `lang.toIdentifier({ name, ...identifierType })`.
    
    Defined in deno/lang-kotlin/src/KtImport.ts:11:1
    
    type KtImportNameArg = string | { name: string; alias?: string; }
      The concise import form a Kotlin generator passes to `register` —
      `'Serializable'` or `{ name: 'User', alias: 'UserModel' }` (Kotlin
      supports symbol-level aliases via `as`, unlike Java). Owned by this
      package: the concise vocabulary is language-specific; the neutral
      engine never sees it. No `type` tag — Kotlin has no type-only imports.
    
    Defined in deno/lang-kotlin/src/KtImport.ts:14:1
    
    type KtImportSpecifier = { name: string; alias?: string; }
      A single imported symbol on a {@link KtImport}.
    
    Defined in deno/lang-kotlin/src/KtParameterList.ts:4:1
    
    type KtParameterArgs = { name: string; type: Stringable; nullable?: boolean; defaultValue?: Stringable; annotations?: KtAnnotation[]; visibility?: "private" | "protected" | "internal"; }
      A single primary-constructor parameter of a Kotlin class.
    
    Defined in deno/lang-kotlin/src/KtPrimaryConstructor.ts:5:1
    
    type KtPrimaryConstructorArgs = { parameters: Stringable; modifiers?: Stringable; }
      Arguments for {@link KtPrimaryConstructor}.
    
    Defined in deno/lang-kotlin/src/register.ts:17:1
    
    type KtRegisterArgs = { imports?: Record<string, KtImportNameArg[]>; definitions?: (DefinitionBase | undefined)[]; custom?: Stringable; }
      Kotlin's concise register vocabulary — the generator-facing form.
    
      Owned by this package: each language defines its own concise args type
      exposing only what the language supports. Kotlin has no re-exports,
      so there is deliberately no `reExports` field — a generator trying to
      register one is a compile-time error, not a runtime no-op (the note-16
      Go example, realized).
    
    Defined in deno/lang-kotlin/src/withDescription.ts:5:1
    
    type WithDescriptionArgs = { description?: string; }
      Arguments for {@link withDescription}.
    ```
    
  • SKILL.md 17.8 KB
    ---
    name: skmtc-lang-kotlin
    version: 0.11.0
    description: >
      The Kotlin target-language layer for Skmtc generators
      (@skmtc/lang-kotlin): base factories, KtSnippet, the seven entity
      kinds, packages-from-paths imports, the head+value render model,
      KtAnnotation and the composition classes, sanitization and
      @SerialName placement, plus the current-API worked example (the
      shipped gen-kotlin-* packages are API-stale — do not copy their call
      shapes). Use ALONGSIDE skmtc-generator whenever a generator emits
      Kotlin. Headings mirror skmtc-lang-typescript.
    metadata:
      internal: true
    ---
    
    # The Kotlin layer (@skmtc/lang-kotlin)
    
    Read `skmtc-generator` first.
    
    > **Drift warning.** The API of record is the workspace
    > `skmtc/deno/lang-kotlin` and its tests. The shipped `gen-kotlin-*`
    > generators predate the 0.9.11 flattening: they call
    > `new KtAnnotation('Name', [args])` positionally and import
    > `isKtAnnotated`/`isKtSupertyped` (no longer exported; supertype
    > clauses now render inline in the value). Clone their **structure**
    > only; take call shapes from THIS skill's example (§8), which is pinned
    > byte-for-byte against the engine by
    > `lang-kotlin/src/skill-example.test.ts`.
    
    ## 1. Declaring the language
    
    Same pattern as TypeScript — the import graph declares it. Two
    factories: `toKtModelProjectionBase`, `toKtOasOperationProjectionBase`;
    snippets extend `KtSnippet`.
    
    ```ts
    export const KtModelBase = toKtModelProjectionBase<EnrichmentSchema>({
      id: denoJson.name,
      toEnrichmentSchema,
      toIdentifierName({ refName, enrichments }) {
        return enrichments?.subject?.name ?? capitalize(camelCase(refName))
      },
      // Kotlin's identifier KIND depends on schema shape → may read context
      // (runs only on cache-miss; the NAME stays pure):
      toIdentifierType(refName, context) {
        return { type: toShape(context, peekSchema(context, refName)) }
      },
      toExportPath({ refName, enrichments }) {
        const name = enrichments?.subject?.name ?? capitalize(camelCase(refName))
        return join('@', ...enrichments.generator.basePackage.split('.'), `${name}.generated.kt`)
      }
    })
    ```
    
    The export path's directory segments ARE the Kotlin package (§4). Make
    `basePackage` a **required generator-scope enrichment** with no default;
    validate segments with `isKtIdentifierName` + `ktHardKeywords`. Put the
    shape dispatch (object+props → `data-class`; string+enums →
    `enum-class`; qualifying discriminated union → `sealed-interface`; else
    `typealias`) in ONE deterministic function read by both
    `toIdentifierType` and the constructor, so kind and value can't disagree.
    
    ## 2. Register shapes — Kotlin differences
    
    Same three shapes as TS (projection own-file / `registerInto` / snippet
    with required `destinationPath`), plus `defineAndRegister` (no cache
    check; no `noExport` — visibility is the identifier's fact: pass
    `exported: false` to the factory). Compile-time differences: **no
    `reExports` field** (Kotlin has none) and **no `type` tag on imports**
    (no type-only imports). `custom` renders above the `package` directive.
    
    ## 3. Identifier kinds
    
    Kotlin output has seven entity kinds (`KtEntityType`): `class`,
    `data-class`, `enum-class`, `interface`, `sealed-interface`,
    `typealias`, `val` —
    factories `createClass`, `createDataClass`, `createEnumClass`,
    `createInterface`, `createSealedInterface`, `createTypeAlias`,
    `createValue` (only `createValue` takes `typeName`; `exported: false`
    renders `private `). Deferred kinds (`object`, `fun`, `var`) make
    `toKtEntityType` throw — deliberately loud. Kind does NOT affect import
    form. The engine's `type` is an opaque string: `isKtEntityType` narrows
    it to the vocabulary above, and `isKtIdentifier` narrows a neutral
    `IdentifierBase` back to `KtIdentifier`.
    
    ## 4. Emitted-import rules
    
    - **Packages from paths**: `@/com/example/api/User.generated.kt` →
      `package com.example.api`. Segments are validated, never sanitized —
      a keyword or invalid segment **throws** (fix the path policy).
    - One `import pkg.Name` per symbol (no brace grouping), `as` aliases,
      rendered **sorted** (determinism, not style).
    - **Same-package suppression is central**: register imports
      unconditionally; `KtFile` drops same-package ones at render.
    - Importing from the default package throws (root-level artifact
      referenced from a packaged one = path-policy bug).
    
    ## 5. Render model: head + value
    
    Assignment kinds (`typealias`, `val`): `<head> = <value>`. Declaration
    kinds: `<head><value>` — the value renders everything after the name:
    parameter list (parens included), inline ` : Parent` clauses, ` { … }`
    bodies; an empty value yields the bodyless idiom
    (`sealed interface Animal`).
    
    Two things ride on value-carried protocols (the neutral Lang signature
    has no slot for them): `KtAnnotated` (`annotations: KtAnnotation[]`,
    strict — string look-alikes are silently dropped) and `KtDocumented`
    (`description`, guard `isKtDocumented`, rendered as KDoc above the
    annotations). **The mirroring gotcha**: the Driver wraps the
    PROJECTION as the definition's value, so mirror both onto the projection
    — canon is **reference assignment in the constructor**
    (`this.annotations = this.value.annotations` — one array, two names;
    never copy) — or class-level annotations and KDoc silently vanish.
    
    ## 6. Composition classes (current API)
    
    - `KtParameterList(parameters)` — parens included; each
      `{ name, type: Stringable, nullable?, defaultValue?, annotations?,
      visibility? }` renders as an indented `val`, annotations one per line.
    - `KtPrimaryConstructor({ parameters, modifiers? })` — modifiers force
      the explicit `constructor` keyword.
    - `KtFunctionSignature({ name, parameters, returnType?, annotations?,
      body? })` — abstract by default, expression body only.
    - `KtAnnotation({ context, name, args?, target?, packageName?,
      destinationPath })` — a **registering leaf**: with `packageName` it
      registers its own import (register unconditionally; suppression
      handles same-package). `args` are pre-quoted (`['"user_id"']`,
      `['Foo::class']`). `target` is the use-site target
      (`KtAnnotationTarget`: `field`/`get`/`set`/…) rendered as
      `@field:JsonAnySetter` — the imported symbol stays the bare `name`.
      Needed on a constructor `val`, which is parameter/property/field/
      getter at once: Jackson's catch-all pair is `@field:JsonAnySetter` +
      `@get:JsonAnyGetter`, and without targets both annotations land on
      the parameter, where Jackson never looks. (Shipped in lang-kotlin
      0.10.0, 2026-08-04 — pre-`target` versions cannot express use-site
      targets at all.)
    - `withDescription(value, { description })` — KDoc.
    
    ## 7. Sanitization and @SerialName
    
    `sanitizePropertyName(name)`: plain → unchanged; hard keyword or invalid
    → **backticked**; JVM-unescapable characters → **throws** ("rename +
    @SerialName"). Renames are NOT its job — serialization annotations
    handle wire-name mismatches, and the two compose: decide the annotation
    by comparing the *unescaped* chosen name with the wire key
    (`` `object` `` needs no @SerialName; `user_id`→`userId` does). Only the
    28 hard keywords escape; soft/modifier keywords (`value`, `data`,
    `sealed`) are legal identifiers. Canonical pairing:
    `sanitizePropertyName(camelCase(key))`.
    
    ## 8. Worked example — kotlinx data class (current API, engine-pinned)
    
    Per-property loop inside the data-class value snippet:
    
    ```ts
    const propertyName = sanitizePropertyName(camelCase(key))
    const annotations: KtAnnotation[] = []
    if (propertyName.replaceAll('`', '') !== key) {
      annotations.push(new KtAnnotation({
        context, destinationPath,
        name: 'SerialName', packageName: 'kotlinx.serialization', args: [`"${key}"`]
      }))
    }
    parameters.push({
      name: propertyName,
      type: value,                     // the SNIPPET — never `${value}`
      defaultValue: isRequired ? undefined : 'null',
      annotations
    })
    // this.parameterList = new KtParameterList(parameters)
    // class-level: this.annotations = [new KtAnnotation({ context,
    //   destinationPath, name: 'Serializable', packageName: 'kotlinx.serialization' })]
    // projection mirrors by REFERENCE: this.annotations = this.value.annotations
    ```
    
    Renders (verified byte-for-byte through the engine):
    
    ```kotlin
    package com.example.api
    
    import kotlinx.serialization.SerialName
    import kotlinx.serialization.Serializable
    
    @Serializable
    data class User(
        @SerialName("user_id")
        val userId: String,
        val name: String,
        val email: String? = null
    )
    ```
    
    The type expression is the **single owner** of `?`; the parameter layer
    only adds `= null`. Passing `` `${value}` `` instead of the snippet
    strands its registered imports and synthesized siblings — the file
    breaks far from the cause. Serialization flavor is confined to the value
    files (data class / enum entries / sealed interface): a Jackson/Moshi
    sibling generator swaps annotation construction there only.
    
    ## 8b. Normalized models — KNOWN ENGINE GAP (verified 2026-08-03)
    
    The head+value model means a Kotlin value renders differently in TYPE
    position (`Map<String, Any?>`) and DECLARATION position (a parameter
    list). Core's generic `insertNormalizedModel` glues the identifier
    head to the value's type-position `toString()` — which for an inline
    OBJECT schema renders invalid Kotlin: `data class XMap<String, Any?>`.
    The engine gap is real, but the SOLUTION does not wait for it —
    every mature Kotlin generator solves inline objects the same way:
    
    - **Named `$ref` schemas are unaffected** — `insertModel` and the ref
      path work correctly.
    - An inline NON-object schema normalizes fine as a `typealias`-shaped
      value.
    - **An inline object is SYNTHESIZED as a named sibling declaration**
      and referenced by name — the retired gen-kotlin-kotlinx pattern
      (`KtObjectValue`, skmtc-generators history at `2c24a65`) rebuilt
      WITHOUT its naming-hint threading: the name derives from the
      schema's own `stackTrail` (`toSynthesizedName.ts` in
      gen-kotlin-jackson — anchor on the `components`/`paths` landmark
      frames, never absolute indices; classification is POSITIONAL:
      `properties` consumes the following frame as a literal key, so a
      property named `properties`/`schema`/`items` can never be mistaken
      for trail structure), so every construction path — including peers
      arriving through `insertNormalizedModel` — lands on the same name
      with NO parameter added to the router contract. Names are NOT
      collision-free: claim via the document-wide registry
      (`claimSynthesizedName`, gen-kotlin-jackson `synthesizedNames.ts`)
      BEFORE declaring — it throws per-item when the name collides with a
      component-derived class name (Kotlin's redeclaration scope is the
      PACKAGE, not the file) or with a different position's claim
      (camelCase-convergent keys), and returns reuse for a same-position
      re-walk. On `'declare'`, `defineAndRegister` the sibling and render
      only the NAME. Type position then always holds a name or a map —
      never property structure. This is also how OpenAPI Generator solves it
      (inline schemas hoisted to named components before generation).
      Widening a known shape to `Map<String, Any?>` is capitulation, not
      a solution — it discards the type the schema gave you. Inline
      string enums synthesize the same way (`enum class` sibling).
    - Never fabricate a refName or drive the peer's identity statics to
      force a declaration into existence — that is the two-doors rule
      (skmtc-generator §4), and the result couples you to the peer's
      private snippet shape.
    
    ## 8c. Discriminated unions — sealed interfaces (shipped 2026-08-04)
    
    Kotlin has no union type; a QUALIFYING discriminated union becomes a
    `sealed interface` (gen-kotlin-jackson is the worked example; ancestry:
    the retired kotlinx machinery at skmtc-generators `2c24a65`, stale call
    shapes). Predicate (`shape.ts isSealedUnion`, part of the shape
    dispatch): discriminated, ≥2 members, every member a `$ref` to an
    object-with-properties, and every member keeps ≥1 parameter AFTER
    discriminator omission. Everything else renders the honest wire type
    (`JsonNode` for Jackson), never `Any`.
    
    - **The inversion scan.** OpenAPI points parent → member; Kotlin
      declares member → parent (`data class Dog(...) : Pet`). Memoization
      makes build order arbitrary, so membership must be known BEFORE any
      construction: one document-wide scan over `components.schemas`,
      memoized per document via `WeakMap`, mapping member refName → claims.
      Claims store the parent's real `RefName`; the consumer derives the
      display name via `context.toModelContentSettings` — never a copy of
      the naming policy, never a fabricated refName.
    - **Parent side**: an empty-body value (`toString()` returns `''` →
      the bodyless idiom) carrying `@JsonTypeInfo(use = NAME, include =
      PROPERTY, property = "<discriminator>")` + `@JsonSubTypes(Type(value
      = Dog::class, name = "dog"), …)` via the `KtAnnotated` protocol —
      mirror `annotations` AND `description` on the projection by
      reference. Each subtype entry holds the walked member ref SNIPPET,
      so member models build and imports stitch through the normal chain.
      Tags: `discriminator.mapping` key pointing at the member, else the
      member's refName (the OpenAPI default).
    - **Member side**: inline ` : Pet` supertype clause rendered by the
      parameter-list value (after the parens), and the discriminator
      property OMITTED — filtered BEFORE the property walk, or its enum
      schema synthesizes a spurious sibling. Same package by the
      export-path policy satisfies Kotlin's sealed same-package rule.
    - **Jackson vs kotlinx flavor**: tags are parent-side
      (`@JsonSubTypes`), so members carry no tag annotation and one member
      may hold different tags under different parents (the kotlinx
      one-`@SerialName`-per-class conflict rule does not apply).
    - **Runtime gotcha (probed)**: a raw `writeValueAsString(list)` erases
      the element type and silently DROPS the tags; concrete roots,
      `writerFor(type)`, and full-generic types all write them — Spring
      MVC uses the typed path, so real consumers are fine. Test round-trips
      with a typed writer.
    - `allOf`-composed members (the spec's canonical idiom: shared fields
      on a base, members compose via `allOf`) qualify WITHOUT special
      handling — core resolves `allOf` at parse time (`mergeIntersection`),
      so the member peeks as a flat object and the base's fields flatten
      into each data class (verified through the pipeline 2026-08-04).
      Flattening is the right Kotlin target: the sealed interface is the
      polymorphism seam, not class inheritance.
    - **Inline unions (stage 2, shipped)**: a qualifying union ANYWHERE —
      component property, operation body/response/header/parameter —
      synthesizes its sealed parent under its stackTrail name (combinator
      frames `oneOf`/`anyOf`/`allOf` are structural and elided; a
      `parameters/<index>` position resolves to the parameter NAME via a
      WeakMap document scan — the trail itself cannot carry names, it
      doubles as a JSON Pointer where `parameters` is an array) into the
      MODELS package (`toModelExportPath` — ONE placement policy for EVERY
      synthesized declaration; caller's-file placement breaks
      `'reuse'`-across-files for cross-package peers). The scan deep-walks
      components AND operations AND webhooks (headers and the `content`
      alternative included); synthesized claims carry the union NODE so
      `ensureSealedParent` lets WHOEVER needs the name first declare it via
      the claim registry. Derivability is ONE shared non-throwing probe
      (`toSynthesizedNameOrNull`) across scan/render/members — underivable
      roots degrade consistently to pre-synthesis behavior; the object/enum
      sites deliberately keep the THROWING derivation (no honest fallback
      exists for structure). One member may implement several sealed
      parents (parent-side tags).
    - Not yet built: undiscriminated unions (stage 3 —
      enrichment-asserted hints / Jackson `Id.DEDUCTION`) and the INVERTED
      swagger-style pattern (discriminator on the base, no `oneOf`,
      membership implied by `allOf` back-references — no union node exists,
      so no sealed interface).
    
    ## 9. Kotlin pitfalls
    
    | Symptom | Fix |
    |---|---|
    | `@Serializable`/KDoc missing | Mirror `annotations`/`description` getters on the projection |
    | Annotation silently dropped | Real `KtAnnotation` instances, not strings |
    | `segment 'x' is not a valid package name part` | Fix the export-path policy — packages validate, never sanitize |
    | Import mid-file / duplicated | `register` / annotation `packageName`, never templates |
    | `String??` | Type expression owns the single `?` |
    | `Unknown Kotlin entity type` | Use the seven Kotlin factories, not TS kinds |
    | Empty `data class` throws | Shape dispatch must route empty objects to `typealias` |
    | TDZ crash at module load | Break base↔router↔projection cycles with a leaf module (`peekSchema` pattern) |
    | Nondeterministic output | No module state; config via enrichments; memoize document scans in `WeakMap` |
    | `data class X` glued to `Map<String, Any?>` | The normalized-insert type/declaration gap — §8b, don't hack around it |
    | Union renders `Any`/`JsonNode` where a sealed type was expected | Qualifying predicate failed — check discriminator presence, all-ref members, per-member surviving parameters (§8c) |
    | Member missing ` : Parent` / spurious discriminator enum sibling | Membership scan not consulted before construction, or omission applied after the property walk (§8c) |
    | Sealed round-trip loses the wire tag at runtime | Jackson root-list type erasure — serialize via a typed writer; generated code is correct (§8c) |
    
    <!-- api-appendix:begin — GENERATED, do not edit by hand -->
    
    ## Appendix — generated API reference
    
    The full `deno doc` surface for the packages this skill covers lives
    in [`appendix.md`](appendix.md), in this skill's directory —
    generated from framework source — signatures and field docs only.
    It is **authoritative**: when the prose above does
    not carry the exact constructor or field shape you need, Read (or
    grep) `appendix.md` instead of diving into package source. Do not
    guess signatures. For a symbol not listed there,
    `deno doc <file> <Symbol>` against the framework source beats
    grepping it.
    
    <!-- api-appendix:end -->
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related