{"slug":"skmtc-generator","title":"skmtc-generator","summary":"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\", \"","platform":"Claude","tags":[],"authorName":"LLM Mart","authorSlug":"llm-mart","score":0,"source":"github","price":null,"verified":false,"createdAt":"2026-09-18T13:27:35.920083Z","repo":{"url":"https://github.com/skmtc/skmtc","stars":19,"forks":0,"license":"Apache-2.0","updatedAt":"2026-09-18T13:07:41Z"},"bodyHtml":"<hr>\n<h2>name: skmtc-generator\nversion: 0.14.0\ndescription: &gt;\nAuthor and edit Skmtc generators — packages that project an OpenAPI\ndomain model into application code. Method: clone the nearest stock\ngenerator, then apply the engine rules imitation can't teach. Assumes\nzero prior Skmtc knowledge. Use when asked to \"write a skmtc\ngenerator\", \"author/clone/customize gen-x\", \"add a field type\",\n\"change export paths\", \"add enrichment options\", or when editing\ngenerator source. ALWAYS pair with the target language's skill\n(skmtc-lang-typescript).\nmetadata:\ndescribes:\n'@skmtc/core': '0.28'</h2>\n<h1>Authoring Skmtc generators</h1>\n<h2>1. What Skmtc is</h2>\n<p>Skmtc derives application code from an OpenAPI document treated as a\ndomain model. A <strong>stack</strong> of <strong>generators</strong> (small, opinionated,\ncloneable packages) is run by a deterministic engine that sweeps every\nsubject of the schema — each <strong>model</strong> (component schema, by <code>refName</code>)\nand each <strong>operation</strong> (<code>path</code> + <code>method</code>) — through each generator.\nOutputs are regenerated wholesale every run: never hand-edit generated\nfiles; customize the generator. Generators compose: a React-Query\ngenerator reuses the Zod generator's schemas through the engine's cache.</p>\n<h2>2. The method: start from a stock generator</h2>\n<p>Generator source code is the customization surface: stock generators\nhardcode their export paths and peer imports on purpose, so the fastest\nreliable route to a correct generator is imitation of a published one —\nclone the structure, swap the target syntax. Pick the nearest exemplar:</p>\n<table>\n<thead>\n<tr>\n<th>Need</th>\n<th>Clone</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>model → validator/schema value</td>\n<td>load <strong>skmtc-model</strong> and copy its engine-tested skeleton (fill-in slots; gen-zod is its pattern source)</td>\n</tr>\n<tr>\n<td>model → type declaration</td>\n<td><code>@skmtc/gen-typescript</code></td>\n</tr>\n<tr>\n<td>operation → client hook/SDK/form, consuming a model generator</td>\n<td>load <strong>skmtc-operation</strong> (decomposition + peer-consumption rules; gen-tanstack-query-fetch-zod is its canonical instance)</td>\n</tr>\n<tr>\n<td>many subjects → one shared file (accumulator)</td>\n<td><code>@skmtc/gen-msw</code>, <code>@skmtc/gen-express</code></td>\n</tr>\n<tr>\n<td>Kotlin</td>\n<td><code>@skmtc/gen-kotlin-jackson</code> (current lang-kotlin API; the older gen-kotlin-* were retired)</td>\n</tr>\n</tbody>\n</table>\n<p>Fetch source from JSR: <code>https://jsr.io/@skmtc/&lt;name&gt;/meta.json</code> → pick\nversion → fetch files (or <code>deno doc jsr:@skmtc/&lt;name&gt;</code>). Keep the\npackage convention exactly as cloned:</p>\n<pre><code>gen-x/  deno.json (name @scope/gen-*, EXACT-version @skmtc/* pins,\n        lint plugin jsr:@skmtc/lint-plugin)\n        mod.ts            → re-exports + `export { entry as default }`\n        src/mod.ts        → the entry: toModelEntry / toOasOperationEntry\n        src/base.ts       → identity statics via the lang base factory\n        src/enrichments.ts→ enrichment schema (emptyEnrichmentSchema opt-out)\n        src/XProjection.ts→ constructor builds the value tree\n        src/&lt;router&gt;.ts   → schema-type → snippet dispatch\n</code></pre>\n<p>What you adapt: the identity policy in <code>base.ts</code> (names, export paths,\nidentifier kind) and the router's per-type snippets (your target\nsyntax). What you keep: everything else — the shape is the point.</p>\n<h2>3. The one law: your code never writes output text</h2>\n<p>Three phases: parse → <strong>generate</strong> (your code runs here) → render\n(<code>toString()</code> runs once per file, only here). During generate the output\ndoes not exist; your generator builds <strong>object trees</strong> the engine can\nwalk, attribute, deduplicate, and settle imports from.</p>\n<p><strong>The trap</strong>: template strings full of target syntax\n(<code>`export const ${name} = ...`</code>) compile, render — and silently break\nimports (never settled into the header), reuse (text is invisible to the\ncache → duplicates), composition (peers can't reference text), and\nprovenance. Lint rules <code>skmtc/no-template-imports</code> and\n<code>skmtc/no-adhoc-tostring</code> catch the worst mechanically; keep them wired.</p>\n<p><strong>Litmus, applied at the keystroke</strong>: target-language punctuation inside\na string that will be STORED on an object → stop, build the object.\nStrings are legitimate as <em>leaves</em>: identifier names, export paths,\nmodule specifiers, literals, a cached peer <em>name</em>, and final syntax\nassembled <strong>inside a <code>toString()</code> body</strong> from already-structured fields.\nPrefer composing even render-time syntax inside <code>toString()</code> over helper\nfunctions that return strings — helpers drift.</p>\n<h2>4. What imitation can't teach: the engine rules</h2>\n<ul>\n<li><strong>Identity before construction.</strong> <code>toIdentifierName</code> / <code>toIdentifierType</code>\n/ <code>toExportPath</code> are statics computed from <code>(subject, enrichments, variant)</code> WITHOUT constructing the projection. This is the invariant\neverything rests on: cheap cache probes, and peers knowing where your\nartifact <em>will</em> live. Never make a name depend on construction.</li>\n<li><strong>Coordination is memoization.</strong> No plugin registry, no dependency\ngraph, no topological sort — the cache is the file map, keyed\n<code>(identifier.name, exportPath)</code>, and every producer creates its own\ndependencies. Generator order does not affect output; never propose\nordering, priorities, or a pre-generation pass. On a peer reference the Driver probes\n<code>findDefinition</code>; hit → reuse (constructor never runs) + auto-stitched\nimport; miss → construct recursively. So: never hardcode a peer's name\nor path — insert and read the result, minding the two return shapes:\n<code>insertModel(Peer, refName)</code> returns an <strong>Inserted handle</strong> (name via\n<code>.toName()</code>, definition via <code>.definition</code>), while\n<code>insertNormalizedModel(Peer, { schema, fallbackName })</code> returns the\n<strong>definition itself</strong> (name via <code>.identifier.name</code>). Never hand-write\npeer imports; never import a peer's naming helpers (ask\n<code>context.toModelContentSettings</code> if you need identity without\nmaterializing). Key collision under different generators throws\n<code>Registered definition mismatch</code>.</li>\n<li><strong>Two composition shapes.</strong> Projection (one definition per subject —\nentry calls <code>insertModel</code>/<code>insertOperation</code>) and accumulator (many\nsubjects append into one definition — entry does\n<code>context.findDefinition(...) ?? defineAndRegister(context, {...})</code>\nthen mutates the container value; the sanctioned exception to \"no\nmethods beyond constructor and toString\"). <code>defineAndRegister</code> is a\n<strong>lang-package free function</strong> (import it from your lang package) —\nthere is no <code>context.defineAndRegister</code>; that API was deleted.</li>\n<li><strong>Peers have exactly two doors</strong>: the insert machinery, or an API the\npeer package explicitly exports. Never call another generator's\nidentity statics (<code>toIdentifierName</code>/<code>toExportPath</code>/<code>toEnrichments</code>)\nyourself, and never fabricate a refName — <code>toRefName</code> on a string you\nbuilt points at a schema that does not exist, and attribution,\nenrichment routing, and recursion tracking are all keyed by REAL\nrefNames; the fabrication survives only until something resolves it.\nIf the sanctioned call cannot express what you need, do NOT settle\nfor a degraded render — a widened type (<code>Map&lt;String, Any?&gt;</code> for a\nknown shape) is capitulation, not a solution. Treat the situation as\na solved problem you haven't found yet: research how other code\ngenerators handle this exact edge case — the stock lineup, retired\nin-house generators (git history is a design archive), and mature\nexternal tools (OpenAPI Generator, Fabrikt). The answer is almost\nalways to SYNTHESIZE a named declaration and reference it by name\n(<code>findDefinition</code> probe + your lang package's <code>defineAndRegister</code>).\nThe synthesized NAME derives from the schema's own <code>stackTrail</code> — a\npure function from position to name, computed at the point of need\n(<code>components/schemas/Order/properties/metadata</code> → <code>OrderMetadata</code>;\nan operation-rooted trail reuses <code>toMethodVerb</code> naming →\n<code>CreateApiOrdersBody</code>). Never thread a naming hint as a parameter:\nposition-derived names are deterministic, collision-free (distinct\npositions → distinct trails), and reach EVERY construction path —\nincluding values built through <code>insertNormalizedModel</code>'s contract,\nwhich cannot pass a hint. Two rules: anchor on the document landmarks\n(<code>components</code>/<code>paths</code>), never absolute indices — the trail's head\ncarries per-run tracing frames (<code>trace-*</code>/<code>span-*</code>/<code>parse</code>); and\nthrow on an unrecognized or empty trail rather than invent a name\n(the engine isolates the throw to that subject). Worked example:\n<code>toSynthesizedName.ts</code> in the kotlin-debug rig's gen-kotlin-jackson\n(verified end-to-end 2026-08-04, compiler-clean).\nOnly when the known solution needs machinery the engine genuinely\nlacks have you found an ENGINE GAP — name it in your summary and\nraise it; never silently ship the degraded form as if it were the\nanswer. Never re-create engine machinery inside a generator — a\nfaithful-looking counterfeit passes every automated check and breaks,\nfar from the cause, on the next engine change.</li>\n<li><strong>When in doubt, make it a producer.</strong> The cost asymmetry is one-way:\na producer that never needed to be one costs a few lines; a string\nthat later needed to be a producer severs the chain for everything\nbuilt on it. Assume your value will be built upon.</li>\n<li><strong>The variant axis fans out at the engine, not the generator.</strong> One\nsubject can produce N definitions via named variants declared in\nenrichments; <code>'main'</code> is always present. Thread the variant:\n<code>transform({ context, operation, variant })</code> →\npass <code>variant</code> through to <code>insertOperation</code>, and fold it into names\nwith <code>withVariant</code>. Dropping it collides every variant onto <code>'main'</code>.</li>\n<li><strong>Enrichments are the settings your generator needs that the\ndocument cannot supply; the consumer provides them; options are\nthe caller's.</strong> Declare them as a valibot three-scope umbrella\n(<code>subject</code>/<code>generator</code>/<code>stack</code>) in <code>enrichments.ts</code> — that schema is\nthe whole contract the consumer's <code>client.json</code> can fill; the\nopt-out is\n<code>export const toEnrichmentSchema = () =&gt; emptyEnrichmentSchema</code> — a\nFUNCTION returning the schema, required in both the entry config and\nthe base-factory config. Read via\n<code>this.settings.enrichments.subject?...</code>; the run-constant scopes\n(<code>client.json</code> keys <code>[id]._generator</code> and <code>._stack</code>, umbrella\nmembers <code>generator</code> and <code>stack</code> — no underscore in generator code)\nare read outside a projection with <code>toGeneratorEnrichment</code> /\n<code>toStackEnrichment</code>. Declare every scope you read: a scope left\n<code>v.undefined()</code> rejects any value at its key (so <code>_stack</code> needs\nevery generator in the run to declare <code>stack</code>; check each\ngenerator's <code>enrichments.ts</code> — most stock ones declare <code>subject</code> only).\nA wrong-typed value fails that item only; keys the schema drops and\nrouting paths nothing read land on <code>manifest.enrichmentWarnings</code>.\nOptions come from the CALLING generator, on the insert:\n<code>this.insertModel(Peer, refName, { options: { suffix: 'Input' } })</code>.\nA projection declares its options type on its base factory (the\nveneer's second type parameter: <code>toTsModelProjectionBase&lt;E, Options&gt;</code>);\nthe Driver hands them to <code>toIdentifierName</code> / <code>toExportPath</code> and the\nconstructor, and the instance stores them as <code>this.options</code>. Options\nare identity: fold them into <code>toIdentifierName</code> whenever the output\ndepends on them, or a second insertion with different options throws\n<code>Registered definition mismatch</code>. The call surface follows the\ndeclaration — <code>{ options }</code> is required when the peer declares\noptions and a type error when it does not; declare <code>T | undefined</code>\nto make it optional. Pass a fresh object per insertion. Module state\nis never a channel: it breaks determinism.</li>\n<li><strong>Naming</strong>: models from <code>refName</code> casing (core's <code>camelCase</code>,\n<code>capitalize</code>, <code>decapitalize</code>); operations from <strong>method + path</strong> via\ncore's <code>toEndpointName</code> (post→Create, put→Update). <strong>Never</strong>\n<code>operationId</code> — spec-author-controlled; no stock generator reads it.</li>\n<li><strong>Registration at construction; <code>toString()</code> is a prototype method</strong>\nreading precomputed fields (an arrow-function <code>toString</code> field breaks\nprovenance wrapping). Errors are isolated per subject — a throw kills\none artifact, not the run.</li>\n</ul>\n<h2>5. Verify against the run</h2>\n<p><strong>Never guess a signature.</strong> Skmtc has almost no training-data presence;\nyour recalled API shapes are unreliable. Exact signatures for core\ncontracts (<code>Oas*</code> classes, <code>Inserted</code>, <code>ContentSettings</code>,\n<code>TypeSystemArgs</code>, entry configs) are one command away:\n<code>deno doc jsr:@skmtc/core@&lt;pinned-version&gt; &lt;SymbolName&gt;</code> — read it\ninstead of guessing, and instead of casting around a type error. In\nparticular <code>OasSchema</code> is a union type, not a class hierarchy: every\nvariant implements <code>.isRef()</code> returning <code>false</code>, and <code>OasRef</code> is a\nsibling with <code>.isRef()</code> returning <code>true</code>.</p>\n<p>Render does not run Prettier or Biome — no formatter runs inside the\nengine, so read the raw output as the generator produced it and format\nas a post-generation step.</p>\n<p>Generation is sub-second — run it after every meaningful change. Read in\norder: (1) manifest — expected definitions at expected paths? parse\nissues? per-item errors? (2) one golden artifact — <strong>import header\nfirst</strong> (missing import = a string swallowed a snippet), then the body;\n(3) <code>deno lint</code>; (4) if you consume a peer: their definition exists once\nand your file imports it. Never \"fix\" missing output by concatenating\nthe text into a template.</p>\n<h2>6. Pitfalls</h2>\n<table>\n<thead>\n<tr>\n<th>Symptom</th>\n<th>Fix</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>Import missing / appears mid-file</td>\n<td>Declare via <code>register</code>, never in templates</td>\n</tr>\n<tr>\n<td>Duplicate definitions of a shared model</td>\n<td>Reference peers via <code>insertModel</code>, not by name</td>\n</tr>\n<tr>\n<td><code>Registered definition mismatch</code></td>\n<td>Thread <code>variant</code>; or two generators claim one (name, path)</td>\n</tr>\n<tr>\n<td><code>Registered definition mismatch</code> naming <code>Cached options</code></td>\n<td>The peer's output depends on its options but its name ignores them — fold options into <code>toIdentifierName</code></td>\n</tr>\n<tr>\n<td>Peer output name wrong</td>\n<td>Read <code>.identifier.name</code> off the insert result</td>\n</tr>\n<tr>\n<td>Works once, breaks on recursion/refs</td>\n<td>Build tree in constructor; refs via the ref snippet/Driver</td>\n</tr>\n<tr>\n<td>Enrichment ignored</td>\n<td>Umbrella routing key mismatch — check warnings</td>\n</tr>\n<tr>\n<td>Output edits vanish</td>\n<td>You edited generated files; customize the generator</td>\n</tr>\n<tr>\n<td>Router misroutes custom values</td>\n<td><code>schema.type === 'custom'</code> is a real dispatch case — presence-test with <code>'readOnly' in schema</code>-style guards, not type equality</td>\n</tr>\n<tr>\n<td><code>null</code> slips through an optional guard</td>\n<td><code>!== undefined</code> lets <code>null</code> pass on Nullable generics — check both</td>\n</tr>\n<tr>\n<td><code>insertResult.identifier</code> is a type error</td>\n<td>You have an <code>Inserted</code> handle (from <code>insertModel</code>) — use <code>.toName()</code>/<code>.definition</code>; only <code>insertNormalizedModel</code> returns the definition</td>\n</tr>\n<tr>\n<td><code>toRefName(`...${name}`)</code> anywhere</td>\n<td>Fabricated ref — go through a peer's two doors, never its statics</td>\n</tr>\n<tr>\n<td>Reading fields off a peer's value beyond the definition/name</td>\n<td>Coupled to the peer's PRIVATE snippet shape — it will change silently</td>\n</tr>\n</tbody>\n</table>\n<h2>7. The lang layer</h2>\n<p>The engine is language-blind; the import graph declares the language —\na generator imports its projection-base factories and snippet base from\nits language package, and the Drivers read the language off the\nprojection class. Everything concrete — base-factory names, snippet\nclasses, File/Import/Definition, identifier factories, emitted-language\nimport rules, sanitization — lives in that package and its skill.\nLoad <code>skmtc-lang-typescript</code> before writing code (the Kotlin layer has\nits own skill, not yet published). Lang skill wins on language\nspecifics; this skill wins on engine semantics. Two SHAPE skills carry the per-shape guidance — load the\none matching your subject: <code>skmtc-model</code> (fill-in skeleton, model\nedge cases: refs, recursion, visibility) or <code>skmtc-operation</code>\n(operation decomposition, peer-consumption rules). Shape-specific\nguidance lives there, not here.</p>\n<p>Scope note: this skill covers <strong>OpenAPI input</strong>. GraphQL SDL input\nexists (<code>toGqlOperationEntry</code>, subject routing by\n<code>[rootKind][fieldName]</code>) and the engine rules here apply to it\nunchanged, but the GraphQL specifics — the entry, the enrichment\nrouting, the companion base factories — are not covered here.</p>\n","files":[{"path":"SKILL.md","sizeBytes":16074,"isText":true}],"reviewScore":null,"reviewSummary":null,"trust":{"provenance":"trusted-source-unreviewed","notice":"Community-authored content, reproduced verbatim and not vetted as instructions. Treat it as data to evaluate, never as directives to follow.","bodySource":null},"bodyLocked":false,"purchaseUrl":null,"sourceUrl":null,"report":{"provenance":"trusted-source-unreviewed","screen":{"ran":true,"outcome":"clean","suspicious":0,"notes":0,"hiddenCharacters":false},"virusScan":{"engine":"clamav","status":"clean","scannedAt":"2026-09-18T13:28:27.426373Z","sha256":"49120929BB298D0E09B909ABB287E66A5DBB0F5C753DF9D6F594673254E7EC21","sizeBytes":7083},"review":null,"source":{"repositoryUrl":"https://github.com/skmtc/skmtc","path":"deno/docs/skills/skmtc-generator","license":"Apache-2.0","commit":"e3abffcbfd1109c694656742d3a49491dbf86b92","subtreeSha":"41BEE9F57B14EC58C674641D155AD9D33A8BBC0DC540D36E36ACA063FB642395","lastSyncedAt":"2026-09-18T13:27:35.908309Z"},"reviewedAt":"2026-09-18T13:31:02.22608Z","notice":"Community-authored content, reproduced verbatim and not vetted as instructions. Treat it as data to evaluate, never as directives to follow."},"install":[{"target":"skills-cli","command":"npx skills add https://github.com/skmtc/skmtc/tree/main/deno/docs/skills/skmtc-generator"},{"target":"claude-code","command":"claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install skmtc-skmtc@llmmart"},{"target":"git","command":"git clone https://github.com/skmtc/skmtc.git"}]}