Claude Cursor Skill

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", "

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

Full trust report

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

Install

skills CLI npx skills add https://github.com/skmtc/skmtc/tree/main/deno/docs/skills/skmtc-generator
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

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:

Need Clone
model → validator/schema value load 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 generator load 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 exactly as cloned:

gen-x/  deno.json (name @scope/gen-*, EXACT-version @skmtc/* pins,
        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/metadataOrderMetadata; 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

Symptom Fix
Import missing / appears mid-file Declare via register, never in templates
Duplicate definitions of a shared model Reference peers via insertModel, not by name
Registered definition mismatch Thread variant; or two generators claim one (name, path)
Registered definition mismatch naming Cached options The peer's output depends on its options but its name ignores them — fold options into toIdentifierName
Peer output name wrong Read .identifier.name off the insert result
Works once, breaks on recursion/refs Build tree in constructor; refs via the ref snippet/Driver
Enrichment ignored Umbrella routing key mismatch — check warnings
Output edits vanish You edited generated files; customize the generator
Router misroutes custom values schema.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 error You have an Inserted handle (from insertModel) — use .toName()/.definition; only insertNormalizedModel returns the definition
toRefName(`...${name}`) anywhere Fabricated ref — go through a peer's two doors, never its statics
Reading fields off a peer's value beyond the definition/name Coupled 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.

Files (skmtc)
  • SKILL.md 15.7 KB
    ---
    name: skmtc-generator
    version: 0.14.0
    description: >
      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).
    metadata:
      describes:
        '@skmtc/core': '0.28'
    ---
    
    # 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:
    
    | Need | Clone |
    |---|---|
    | model → validator/schema value | load **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 generator | load **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 exactly as cloned:
    
    ```
    gen-x/  deno.json (name @scope/gen-*, EXACT-version @skmtc/* pins,
            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
    
    | Symptom | Fix |
    |---|---|
    | Import missing / appears mid-file | Declare via `register`, never in templates |
    | Duplicate definitions of a shared model | Reference peers via `insertModel`, not by name |
    | `Registered definition mismatch` | Thread `variant`; or two generators claim one (name, path) |
    | `Registered definition mismatch` naming `Cached options` | The peer's output depends on its options but its name ignores them — fold options into `toIdentifierName` |
    | Peer output name wrong | Read `.identifier.name` off the insert result |
    | Works once, breaks on recursion/refs | Build tree in constructor; refs via the ref snippet/Driver |
    | Enrichment ignored | Umbrella routing key mismatch — check warnings |
    | Output edits vanish | You edited generated files; customize the generator |
    | Router misroutes custom values | `schema.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 error | You have an `Inserted` handle (from `insertModel`) — use `.toName()`/`.definition`; only `insertNormalizedModel` returns the definition |
    | `` toRefName(`...${name}`) `` anywhere | Fabricated ref — go through a peer's two doors, never its statics |
    | Reading fields off a peer's value beyond the definition/name | Coupled 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.
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related