本文へ移動
cccskills
無料GitHub で公開

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 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.

インストール方法を見る

含まれるファイル(2)

  • SKILL.md17.8 KB
  • appendix.md37.6 KB

SKILL.md(原文)

インストールする前に、エージェントに与えられる指示の中身を確認できます。

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 `@/${enrichments.generator.basePackage.replaceAll('.', '/')}/${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

SymptomFix
@Serializable/KDoc missingMirror annotations/description getters on the projection
Annotation silently droppedReal KtAnnotation instances, not strings
segment 'x' is not a valid package name partFix the export-path policy — packages validate, never sanitize
Import mid-file / duplicatedregister / annotation packageName, never templates
String??Type expression owns the single ?
Unknown Kotlin entity typeUse the seven Kotlin factories, not TS kinds
Empty data class throwsShape dispatch must route empty objects to typealias
TDZ crash at module loadBreak base↔router↔projection cycles with a leaf module (peekSchema pattern)
Nondeterministic outputNo 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 expectedQualifying predicate failed — check discriminator presence, all-ref members, per-member surviving parameters (§8c)
Member missing : Parent / spurious discriminator enum siblingMembership scan not consulted before construction, or omission applied after the property walk (§8c)
Sealed round-trip loses the wire tag at runtimeJackson 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, 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 -->

レビュー

まだレビューはありません。使ってみた感想をお寄せください。

同じリポジトリのスキル

概要と使いどころ

Write, restructure, or review documentation — tutorials, how-to guides, reference pages, concept/explanation docs, API references, READMEs, changelogs, release notes, and troubleshooting guides. Distills documentation craft from Mintlify's guides (compiled from technical writers at Stripe, GitHub, Amplitude, and Anaconda), the Diátaxis framework (including the compass and per-type voice), the Google and Microsoft style guides, Every Page Is Page One, Write the Docs, and Docs for Developers: audience analysis, content-type selection, style and word-level rules, procedure writing, structure for humans and AI agents, code-example standards, page templates (templates.md), mechanical enforcement and llms.txt (mechanics.md), maintenance, and success metrics. Use this skill when the user asks to "write docs", "document this feature", "improve this page", "review these docs", "write a tutorial / how-to / reference page", "structure the docs", "write API documentation", "write a README / changelog", or when authoring any file under a `docs/` tree. For SKMTC docs specifically, this skill governs the *craft* (what makes the page good); the content split between skills, `llms.md`, and the docs tree is governed by `docs/skills/README.md`. Distinct from `skmtc-retro` (captures observations about work) and the `skmtc-*` operational skills (guide doing the work) — this skill guides writing *about* the work for readers.

日本語の概要は準備中です。原文の説明を表示しています。

skmtc/skmtc192026年10月12日 更新

Understand what SKMTC is, how its engine works, and the architectural invariants — for agents building or extending infrastructure *around* SKMTC rather than authoring generators or running the CLI. Covers the three-phase pipeline, the host/Worker boundary, cross-generator coordination, the manifest, the attribution / gen-maps (provenance) subsystem, the package graph, the dependency substrate, and the design decisions that make SKMTC behave unlike a typical codegen tool. Use this skill when the user asks "what is SKMTC", "how does the SKMTC engine work", "explain the SKMTC architecture", or is building platform infrastructure around SKMTC — a hosted generate API, a schema or generator registry, tracing or provenance tooling, a web app or SaaS that wraps the engine, or platform-level CI integration. This skill is the system mental model. It does NOT cover authoring generators (→ skmtc-generator), running CLI commands (→ skmtc-cli), or diagnosing broken runs (→ skmtc-debug).

日本語の概要は準備中です。原文の説明を表示しています。

skmtc/skmtc192026年10月12日 更新

skmtc-cli

無料

Use the Skmtc CLI to scaffold projects, install or clone generators from JSR, configure schema sources and enrichments, and produce code artifacts from an OpenAPI v3 or GraphQL SDL schema. Teaches the workspace mental model (`<root>/.skmtc/<project>/`, client.json, bundle, manifest) and the agent contract (strict text / strict JSON modes, exit codes, recipe errors, `agent-context` + `doctor`); the command surface itself is discovered from the binary — `skmtc --help`, `skmtc <cmd> -h` — rather than carried in this skill. Use this skill when the user asks to "run skmtc", "generate code from an OpenAPI schema", "install a skmtc generator", "scaffold a skmtc project", "watch a skmtc project", "configure enrichments", "publish a stack", "deploy to skmtc-hub" (the command is `publish`; there is no `deploy`), "skmtc in CI", or invokes any CLI subcommand. For *authoring* a generator package (Projections, Snippets, transform functions), defer to `skmtc-generator`. When something is broken (no output, wrong output, error messages, a failed bundle build), verify before proposing a fix: read the manifest and the parse issues, and reproduce the failure first.

日本語の概要は準備中です。原文の説明を表示しています。

skmtc/skmtc192026年10月12日 更新

Diagnose failures in SKMTC sessions — no output, wrong output, error messages, failed bundle builds, parseIssues, "Registered definition mismatch", ref cycles, "Module not found" in generated code, or any other broken behavior. Applies across both CLI usage and generator authoring contexts. Use this skill when the user asks "why isn't my generator working", "no output for X", "wrong output", "what does this error mean", "manifest says X", "bundle failed", "INVALID_SCHEMA", "INVALID_DEPENDENCY_REF", "Registered definition mismatch", "Module not found" (in generated code), "ConfigValidationError", or reports any other SKMTC failure. This skill encodes a **verify-first epistemic stance** — read the manifest, check parseIssues, reproduce the failure before proposing fixes. Distinct from `skmtc-cli` and `skmtc-generator` which guide *doing*; this skill guides *diagnosing*.

日本語の概要は準備中です。原文の説明を表示しています。

skmtc/skmtc192026年10月12日 更新

Author and edit Skmtc generators — packages that project an OpenAPI domain model into application code. Method: clone the nearest stock generator, then apply the engine rules imitation can't teach. Assumes zero prior Skmtc knowledge. Use when asked to "write a skmtc generator", "author/clone/customize gen-x", "add a field type", "change export paths", "add enrichment options", or when editing generator source. ALWAYS pair with the target language's skill (skmtc-lang-typescript).

日本語の概要は準備中です。原文の説明を表示しています。

skmtc/skmtc192026年10月12日 更新

The GraphQL pipeline for SKMTC generators — authoring generators whose input schema is GraphQL SDL rather than OpenAPI. Covers `toGqlOperationEntry`, `GqlOperation`, `synthesizeArgsObject` (mutation args -> object schema), the GQL enrichment routing (`[id][rootKind][fieldName][variant]` — two nested subject keys where OAS has path+method), the `to<Lang>GqlOperationProjectionBase` companion factories, and the `GeneratorKey` shape `id|rootKind|fieldName|variant`. Use this skill ALONGSIDE `skmtc-generator` whenever the schema source is GraphQL SDL or the task mentions "GraphQL", "SDL", "GqlOperation", "toGqlOperationEntry", or GraphQL query/mutation generators. Engine rules (producers, register/insert, the axioms) stay in `skmtc-generator`; this skill carries only what differs for GraphQL.

日本語の概要は準備中です。原文の説明を表示しています。

skmtc/skmtc192026年10月12日 更新

skmtc のスキルをすべて見る

このスキルの問題を報告する