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

skmtc-generator

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

インストール方法を見る

含まれるファイル(1)

  • SKILL.md15.9 KB

SKILL.md(原文)

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

Authoring Skmtc generators

1. What Skmtc is

Skmtc derives application code from an OpenAPI document treated as a domain model. A stack of generators (small, opinionated, cloneable packages) is run by a deterministic engine that sweeps every subject of the schema — each model (component schema, by refName) and each operation (path + method) — through each generator. Outputs are regenerated wholesale every run: never hand-edit generated files; customize the generator. Generators compose: a React-Query generator reuses the Zod generator's schemas through the engine's cache.

2. The method: start from a stock generator

Generator source code is the customization surface: stock generators hardcode their export paths and peer imports on purpose, so the fastest reliable route to a correct generator is imitation of a published one — clone the structure, swap the target syntax. Pick the nearest exemplar:

NeedClone
model → validator/schema valueload skmtc-model and copy its engine-tested skeleton (fill-in slots; gen-zod is its pattern source)
model → type declaration@skmtc/gen-typescript
operation → client hook/SDK/form, consuming a model generatorload skmtc-operation (decomposition + peer-consumption rules; gen-tanstack-query-fetch-zod is its canonical instance)
many subjects → one shared file (accumulator)@skmtc/gen-msw, @skmtc/gen-express
Kotlin@skmtc/gen-kotlin-jackson (current lang-kotlin API; the older gen-kotlin-* were retired)

Fetch source from JSR: https://jsr.io/@skmtc/<name>/meta.json → pick version → fetch files (or deno doc jsr:@skmtc/<name>). Keep the package convention as cloned, with one exception: declare @skmtc/* dependencies as caret ranges (^x.y.z), even where the clone source pins them exactly — an exact core pin puts a second copy of core in the bundle as soon as the project pins a different core:

gen-x/  deno.json (name @scope/gen-*, caret-range @skmtc/* pins (^x.y.z),
        lint plugin jsr:@skmtc/lint-plugin)
        mod.ts            → re-exports + `export { entry as default }`
        src/mod.ts        → the entry: toModelEntry / toOasOperationEntry
        src/base.ts       → identity statics via the lang base factory
        src/enrichments.ts→ enrichment schema (emptyEnrichmentSchema opt-out)
        src/XProjection.ts→ constructor builds the value tree
        src/<router>.ts   → schema-type → snippet dispatch

What you adapt: the identity policy in base.ts (names, export paths, identifier kind) and the router's per-type snippets (your target syntax). What you keep: everything else — the shape is the point.

3. The one law: your code never writes output text

Three phases: parse → generate (your code runs here) → render (toString() runs once per file, only here). During generate the output does not exist; your generator builds object trees the engine can walk, attribute, deduplicate, and settle imports from.

The trap: template strings full of target syntax (`export const ${name} = ...`) compile, render — and silently break imports (never settled into the header), reuse (text is invisible to the cache → duplicates), composition (peers can't reference text), and provenance. Lint rules skmtc/no-template-imports and skmtc/no-adhoc-tostring catch the worst mechanically; keep them wired.

Litmus, applied at the keystroke: target-language punctuation inside a string that will be STORED on an object → stop, build the object. Strings are legitimate as leaves: identifier names, export paths, module specifiers, literals, a cached peer name, and final syntax assembled inside a toString() body from already-structured fields. Prefer composing even render-time syntax inside toString() over helper functions that return strings — helpers drift.

4. What imitation can't teach: the engine rules

  • Identity before construction. toIdentifierName / toIdentifierType / toExportPath are statics computed from (subject, enrichments, variant) WITHOUT constructing the projection. This is the invariant everything rests on: cheap cache probes, and peers knowing where your artifact will live. Never make a name depend on construction.
  • Coordination is memoization. No plugin registry, no dependency graph, no topological sort — the cache is the file map, keyed (identifier.name, exportPath), and every producer creates its own dependencies. Generator order does not affect output; never propose ordering, priorities, or a pre-generation pass. On a peer reference the Driver probes findDefinition; hit → reuse (constructor never runs) + auto-stitched import; miss → construct recursively. So: never hardcode a peer's name or path — insert and read the result, minding the two return shapes: insertModel(Peer, refName) returns an Inserted handle (name via .toName(), definition via .definition), while insertNormalizedModel(Peer, { schema, fallbackName }) returns the definition itself (name via .identifier.name). Never hand-write peer imports; never import a peer's naming helpers (ask context.toModelContentSettings if you need identity without materializing). Key collision under different generators throws Registered definition mismatch.
  • Two composition shapes. Projection (one definition per subject — entry calls insertModel/insertOperation) and accumulator (many subjects append into one definition — entry does context.findDefinition(...) ?? defineAndRegister(context, {...}) then mutates the container value; the sanctioned exception to "no methods beyond constructor and toString"). defineAndRegister is a lang-package free function (import it from your lang package) — there is no context.defineAndRegister; that API was deleted.
  • Peers have exactly two doors: the insert machinery, or an API the peer package explicitly exports. Never call another generator's identity statics (toIdentifierName/toExportPath/toEnrichments) yourself, and never fabricate a refName — toRefName on a string you built points at a schema that does not exist, and attribution, enrichment routing, and recursion tracking are all keyed by REAL refNames; the fabrication survives only until something resolves it. If the sanctioned call cannot express what you need, do NOT settle for a degraded render — a widened type (Map<String, Any?> for a known shape) is capitulation, not a solution. Treat the situation as a solved problem you haven't found yet: research how other code generators handle this exact edge case — the stock lineup, retired in-house generators (git history is a design archive), and mature external tools (OpenAPI Generator, Fabrikt). The answer is almost always to SYNTHESIZE a named declaration and reference it by name (findDefinition probe + your lang package's defineAndRegister). The synthesized NAME derives from the schema's own stackTrail — a pure function from position to name, computed at the point of need (components/schemas/Order/properties/metadata → OrderMetadata; an operation-rooted trail reuses toMethodVerb naming → CreateApiOrdersBody). Never thread a naming hint as a parameter: position-derived names are deterministic, collision-free (distinct positions → distinct trails), and reach EVERY construction path — including values built through insertNormalizedModel's contract, which cannot pass a hint. Two rules: anchor on the document landmarks (components/paths), never absolute indices — the trail's head carries per-run tracing frames (trace-*/span-*/parse); and throw on an unrecognized or empty trail rather than invent a name (the engine isolates the throw to that subject). Worked example: toSynthesizedName.ts in the kotlin-debug rig's gen-kotlin-jackson (verified end-to-end 2026-08-04, compiler-clean). Only when the known solution needs machinery the engine genuinely lacks have you found an ENGINE GAP — name it in your summary and raise it; never silently ship the degraded form as if it were the answer. Never re-create engine machinery inside a generator — a faithful-looking counterfeit passes every automated check and breaks, far from the cause, on the next engine change.
  • When in doubt, make it a producer. The cost asymmetry is one-way: a producer that never needed to be one costs a few lines; a string that later needed to be a producer severs the chain for everything built on it. Assume your value will be built upon.
  • The variant axis fans out at the engine, not the generator. One subject can produce N definitions via named variants declared in enrichments; 'main' is always present. Thread the variant: transform({ context, operation, variant }) → pass variant through to insertOperation, and fold it into names with withVariant. Dropping it collides every variant onto 'main'.
  • Enrichments are the settings your generator needs that the document cannot supply; the consumer provides them; options are the caller's. Declare them as a valibot three-scope umbrella (subject/generator/stack) in enrichments.ts — that schema is the whole contract the consumer's client.json can fill; the opt-out is export const toEnrichmentSchema = () => emptyEnrichmentSchema — a FUNCTION returning the schema, required in both the entry config and the base-factory config. Read via this.settings.enrichments.subject?...; the run-constant scopes (client.json keys [id]._generator and ._stack, umbrella members generator and stack — no underscore in generator code) are read outside a projection with toGeneratorEnrichment / toStackEnrichment. Declare every scope you read: a scope left v.undefined() rejects any value at its key (so _stack needs every generator in the run to declare stack; check each generator's enrichments.ts — most stock ones declare subject only). A wrong-typed value fails that item only; keys the schema drops and routing paths nothing read land on manifest.enrichmentWarnings. Options come from the CALLING generator, on the insert: this.insertModel(Peer, refName, { options: { suffix: 'Input' } }). A projection declares its options type on its base factory (the veneer's second type parameter: toTsModelProjectionBase<E, Options>); the Driver hands them to toIdentifierName / toExportPath and the constructor, and the instance stores them as this.options. Options are identity: fold them into toIdentifierName whenever the output depends on them, or a second insertion with different options throws Registered definition mismatch. The call surface follows the declaration — { options } is required when the peer declares options and a type error when it does not; declare T | undefined to make it optional. Pass a fresh object per insertion. Module state is never a channel: it breaks determinism.
  • Naming: models from refName casing (core's camelCase, capitalize, decapitalize); operations from method + path via core's toEndpointName (post→Create, put→Update). Never operationId — spec-author-controlled; no stock generator reads it.
  • Registration at construction; toString() is a prototype method reading precomputed fields (an arrow-function toString field breaks provenance wrapping). Errors are isolated per subject — a throw kills one artifact, not the run.

5. Verify against the run

Never guess a signature. Skmtc has almost no training-data presence; your recalled API shapes are unreliable. Exact signatures for core contracts (Oas* classes, Inserted, ContentSettings, TypeSystemArgs, entry configs) are one command away: deno doc jsr:@skmtc/core@<pinned-version> <SymbolName> — read it instead of guessing, and instead of casting around a type error. In particular OasSchema is a union type, not a class hierarchy: every variant implements .isRef() returning false, and OasRef is a sibling with .isRef() returning true.

Render does not run Prettier or Biome — no formatter runs inside the engine, so read the raw output as the generator produced it and format as a post-generation step.

Generation is sub-second — run it after every meaningful change. Read in order: (1) manifest — expected definitions at expected paths? parse issues? per-item errors? (2) one golden artifact — import header first (missing import = a string swallowed a snippet), then the body; (3) deno lint; (4) if you consume a peer: their definition exists once and your file imports it. Never "fix" missing output by concatenating the text into a template.

6. Pitfalls

SymptomFix
Import missing / appears mid-fileDeclare via register, never in templates
Duplicate definitions of a shared modelReference peers via insertModel, not by name
Registered definition mismatchThread variant; or two generators claim one (name, path)
Registered definition mismatch naming Cached optionsThe peer's output depends on its options but its name ignores them — fold options into toIdentifierName
Peer output name wrongRead .identifier.name off the insert result
Works once, breaks on recursion/refsBuild tree in constructor; refs via the ref snippet/Driver
Enrichment ignoredUmbrella routing key mismatch — check warnings
Output edits vanishYou edited generated files; customize the generator
Router misroutes custom valuesschema.type === 'custom' is a real dispatch case — presence-test with 'readOnly' in schema-style guards, not type equality
null slips through an optional guard!== undefined lets null pass on Nullable generics — check both
insertResult.identifier is a type errorYou have an Inserted handle (from insertModel) — use .toName()/.definition; only insertNormalizedModel returns the definition
toRefName(`...${name}`) anywhereFabricated ref — go through a peer's two doors, never its statics
Reading fields off a peer's value beyond the definition/nameCoupled to the peer's PRIVATE snippet shape — it will change silently

7. The lang layer

The engine is language-blind; the import graph declares the language — a generator imports its projection-base factories and snippet base from its language package, and the Drivers read the language off the projection class. Everything concrete — base-factory names, snippet classes, File/Import/Definition, identifier factories, emitted-language import rules, sanitization — lives in that package and its skill. Load skmtc-lang-typescript before writing code (the Kotlin layer has its own skill, not yet published). Lang skill wins on language specifics; this skill wins on engine semantics. Two SHAPE skills carry the per-shape guidance — load the one matching your subject: skmtc-model (fill-in skeleton, model edge cases: refs, recursion, visibility) or skmtc-operation (operation decomposition, peer-consumption rules). Shape-specific guidance lives there, not here.

Scope note: this skill covers OpenAPI input. GraphQL SDL input exists (toGqlOperationEntry, subject routing by [rootKind][fieldName]) and the engine rules here apply to it unchanged, but the GraphQL specifics — the entry, the enrichment routing, the companion base factories — are not covered here.

レビュー

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

同じリポジトリのスキル

概要と使いどころ

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月3日 更新

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月3日 更新

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月3日 更新

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月3日 更新

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月3日 更新

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.

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

skmtc/skmtc192026年10月3日 更新

skmtc のスキルをすべて見る

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