SKMTC architecture
This skill is the system mental model for agents building infrastructure around SKMTC — a hosted generation service, a schema or generator registry, provenance and tracing tooling, a web app that wraps the engine. It explains what SKMTC is, how it works, and why it behaves the way it does — deeply enough to reason about and extend, without the generator-authoring or CLI-operating detail the sibling skills carry.
SKMTC is counter-intuitive on purpose. Most of what generic codegen and generic backend-infra training data would suggest is actively wrong here. The fastest way to make a bad architectural proposal is to extrapolate from another tool. Read §1 and §11 before proposing anything.
1. The five facts that override default LLM intuitions
These override what training-data priors suggest about codegen tools. They apply across every SKMTC interaction; an infra builder needs facts 1, 2 and 5 most.
No plugin registry, no dependency graph, no topological sort. Cross-generator coordination is a
Mapcache keyed by(identifier.name, exportPath). Generator execution order does not affect output.Render does not run Prettier or Biome. No formatter runs inside
@skmtc/core. Generated output is unformatted by design; consumers format separately.Generator source code is the customization surface. Stock generators have deliberately hardcoded export paths and peer imports. To customize beyond enrichments: clone the generator and edit it. There are no config flags for paths or output shape.
OasSchemais a union type, not a class hierarchy. Sibling classes (OasObject,OasArray,OasString, …) each independently implement.isRef()returningfalse.OasRefis a sibling with.isRef()returningtrue. There is noBaseSchema.Every generation run is from cold. One Deno Worker is spawned per
generate, runs Parse → Generate → Render once, posts the result back, and is terminated. No warm pool, no cross-run cache, no incremental rebuild.toArtifactsis a pure function of(document, settings, generators). Determinism is chosen over speed deliberately — caching belongs outside the engine, keyed on input hashes.
2. What SKMTC is
SKMTC is a code generator. It takes one OpenAPI v3 document or one GraphQL SDL schema and produces source files — types, runtime validators, query hooks, forms, mocks, server routes — all derived from that one schema, in one run, all consistent with each other. The output is committed to the consumer's repository like any other source code; there is zero SKMTC runtime in the consumer's bundle. The engine is language-blind (core 0.8.0+): a generator declares its target language by importing its projection-base factories and snippet base from a
@skmtc/lang-*package — the import graph alone carries the language; entries have nolangfield. TypeScript (@skmtc/lang-typescript) and Kotlin (@skmtc/lang-kotlin, proven bygen-kotlinDTOs +gen-kotlin-springcontrollers) are the production languages; otherlang-*packages (C#, …) are the roadmap.
The crucial reframing for an infrastructure builder: SKMTC is an engine with several thin hosts, not a CLI. The CLI is one host. The mental decomposition:
| Layer | Package | Role |
|---|---|---|
| Engine | @skmtc/core |
The three-phase pipeline. Entry point: core/run/toArtifacts.ts. |
| Worker host | @skmtc/worker |
Wraps the engine in a Deno Worker postMessage handler. |
| CLI host | @skmtc/cli |
Local developer surface: scaffold, install, bundle, generate, watch. |
| HTTP host | @skmtc/server |
Hono app — the hosted "Sandbox API". POST /artifacts. |
| MCP host | @skmtc/mcp |
Model Context Protocol server surface. |
| Schema normalizer | @skmtc/convert, @skmtc/openapi-down-convert |
Swagger 2 / OAS 3.1 → OAS 3.0. Runs before the engine. |
| Generators | @skmtc/gen-* |
The actual codegen logic, distributed as JSR packages. |
Every host does the same thing: get a schema, call toArtifacts,
do something with { artifacts, manifest }. A SaaS is just
another host of the same engine.
3. What you get — the benefits
- Multi-artifact coherence. One schema yields N artifact types (types + validators + hooks + forms + mocks + routes) that reference each other correctly. Add a field, regenerate, every artifact updates consistently.
- Output is committed source code. Reviewable in
git diff, grep-able, refactorable. No runtime library, no peer-dependency package at deploy time. Schema/output drift is visible in version control. - Determinism. Same inputs → byte-identical output. No hidden state, no order dependence, no warm-cache effects.
- Idempotency by construction. Generators coordinate by memoization, not by a dependency graph — so they can be written, tested, and reasoned about in isolation, and order never matters.
- Clone-to-customize. Generators are owned source code, not opaque configured dependencies (the shadcn/ui model).
- Lenient input, strict diagnostics. One malformed schema does
not kill the run; it is logged and its dependents pruned. The
manifest.jsonis an exhaustive record even when output is partial. - OAS and GraphQL through one engine. The GraphQL pipeline reuses the same DSL, the same renderer, the same manifest.
4. When to use SKMTC — and when not to
| Verdict | Situation |
|---|---|
| Strong fit | An OpenAPI v3 or GraphQL schema is the contract; you need multiple artifact types from it; you want generated code committed to the repo. |
| Overkill | You only need types (openapi-typescript); you only need a typed fetch client (@hey-api/openapi-ts); schemas are dynamic at runtime (use a runtime renderer). |
| Wrong tool | Can't run Deno and won't use the hosted Sandbox API; you need production output in a language with no @skmtc/lang-* package yet (today that is everything except TypeScript — use openapi-generator). |
SKMTC's closest peer is kubb — multi-target, TypeScript-native.
The distinguishing bet is the customization model: clones (source
you own) over plugins (configured packages), plus coordination by
name (memoization) over explicit composition. Full landscape:
explanation/comparison-to-other-tools.md.
5. How it works — the pipeline
A generation run is a one-way pipeline of three phases, each producing an immutable artifact the next consumes:
┌─────────────── HOST PROCESS ───────────────┐
│ bootstrap · fetch schema · OAS pre-parse │
└──────────────────────┬──────────────────────┘
│ postMessage(GENERATE)
┌──────────────────────▼──────────────────────┐
│ DENO WORKER (sandboxed) │
│ PARSE ─▶ GENERATE ─▶ RENDER │
│ model files map { path: text } │
│ +issues (in memory) artifacts │
└──────────────────────┬──────────────────────┘
│ postMessage(RESULT)
┌──────────────────────▼──────────────────────┐
│ HOST: write files to disk · write manifest │
└─────────────────────────────────────────────┘
- Parse — schema → typed object model (
OasDocument/GqlDocument). Lenient: a per-item parser that throws becomes aParseIssue;removeErroredItemsthen prunes one hop of$refconsumers of any failed component. Generate can trust every surviving item. - Generate — walk the configured generators over the parsed
document, producing an in-memory
Map<path, File>. Generators produce output via side effects (register/insert*), never by return value. Memoized Drivers dedupe and coordinate (§6). - Render — serialize each
Fileto a string by joining re-exports, imports, and definitions. No formatter runs. Output isRecord<path, content>.
Each phase boundary is an immutable hand-off — the next phase reads,
never mutates. Detail:
concepts/the-three-phases.md.
The host / Worker boundary
The engine physically runs in a Deno Worker spawned by the host,
one per run (fact 5). The boundary is postMessage, which uses the
structured-clone algorithm — and that shapes a real asymmetry:
- OAS is parsed host-side. A plain
OpenAPIV3.Documentis JSON — it survives structured clone. The host normalizes Swagger 2 / OAS 3.1 to 3.0 via@skmtc/convert, then posts the plain document. - GraphQL is parsed Worker-side. A parsed
GqlDocumenthas class instances with cyclic back-references — structured clone strips methods and prototypes. So the host posts the raw SDL string and the Worker parses it.
The Worker boundary is also the security boundary. The Worker is
spawned with Deno permissions read: true, write: true, env: true, net: false, run: false. Generator code (third-party JSR packages or
team-edited clones) cannot make network calls or spawn subprocesses.
It is a soft sandbox — it limits the blast radius of a buggy or
compromised generator, not a determined attacker. Schema fetching
happens host-side, before the Worker exists. Detail:
concepts/the-worker-runtime.md,
explanation/security-model.md.
The engine entry point
Every host calls one function:
toArtifacts({
traceId, spanId, startAt, // run correlation
document, // SkmtcDocumentInput (oas | gql)
settings, // ClientSettings (basePath, enrichments, skip, include)
toGeneratorConfigMap, // () => the registered generators
stackTrail, // position/trace stack
silent,
attribution // optional — turns on gen-maps (§8)
}): { artifacts: Record<string,string>; manifest: ManifestContent;
sidecars?; generationMap? }
It is pure with respect to its inputs and does no I/O of its own
beyond reading. core/run/toArtifacts.ts, worker/mod.ts, and
server/src/createServer.ts are three independent callers of it —
study them as the templates for a fourth.
6. How it works — cross-generator coordination
This is the single most counter-intuitive piece. There is no dependency graph and no topological sort. Coordination is memoization:
- A generator that needs a peer's output calls
this.insertOperation(PeerProjection, op)(orinsertModel/insertNormalizedModel). - A Driver (
OasOperationDriver,GqlOperationDriver,ModelDriver) computes a cache key(identifier.name, exportPath)— both pure functions of(operation, enrichments)via the peer's static methods. - Cache hit → the existing
Definitionis reused. Miss → the peer's Projection is constructed (which may recurse), wrapped in aDefinition, registered, and its import stitched into the calling file.
Because the key is a pure function of inputs, whichever generator
asks first triggers construction and everyone after gets a cache
hit — so the output Map<path, File> is identical regardless of
generator order. A generatorKey integrity check throws
"Registered definition mismatch" if two different
generator-and-input pairs collide on one cache key.
Why this matters for infra: order-independence means there is
nothing to schedule or sequence. Determinism means the correct
caching layer is outside the engine — cache the whole
{ artifacts, manifest } result keyed on a hash of
(schema, settings, bundle). Never try to cache inside a run.
Detail:
concepts/cross-generator-coordination.md.
7. The DSL in one screen
You do not need to author generators to build infrastructure, but
you should recognize the vocabulary (defer authoring to
skmtc-generator):
- A generator is a JSR package exporting an entry built with
toOasOperationEntry/toGqlOperationEntry/toModelEntry. Entries are pure pipeline config — nolangfield; the generator declares its target language by importing its projection bases from a@skmtc/lang-*package, and the engine's Drivers read it off the projection class's inherited static. - The entry's
transformhook runs once per matched operation/model and produces output by callingregister/insert*— its return value is discarded. - A Projection is a named, file-scope artifact (
export const X = …), wrapped in aDefinition, cached by(name, exportPath), reachable by other generators. A Snippet is an anonymous fragment embedded into a Projection's body via template-literal interpolation. - Templates are TypeScript template literals inside classes — not
.hbs/.mustachefiles. Composition is by${...}interpolation of anythingStringable. (Generators are authored in TypeScript/Deno regardless of the target language they emit.)
Vocabulary discipline: in SKMTC prose use register, insert,
render. Avoid emit, dispatch, stitch — they map to no
exported surface. See
reference/glossary.md.
8. The manifest — the run contract and tracing
Every run writes a manifest.json (to
.skmtc/<project>/.settings/manifest.json for the CLI host;
returned in-band for the HTTP host). It is the canonical record of
a run — written always, even on failure. The terminal output is
a summary; the manifest is the full story.
type ManifestContent = {
deploymentId: string // the run; from DENO_DEPLOYMENT_ID or a timestamp
traceId: string // OpenTelemetry-shaped correlation key
spanId: string // sub-span within the run
region?: string // set only for hosted (Deno Deploy) runs
files: Record<path, { lines, characters, destinationPath }>
results: ResultsItem // nested tree of per-(generator × item) outcomes
previews, mappings // optional UI metadata (per-Definition source descriptors)
parseIssues: ParseIssue[] // every Parse-phase diagnostic
startAt, endAt: number // unix ms; endAt - startAt = worker wall time
}
What an infra builder must internalize:
- Tracing is built in.
traceId/spanIdare OpenTelemetry-shaped. TheStackTrailcarries them as its root frames, and theresultstree is keyed by stack-trail strings — so an outcome is addressable astraceId → spanId → generate → generatorId → item.deploymentIdandregioncome from Deno Deploy env vars on the hosted path. Wire these straight into an observability backend. - Exit / status derives from
parseIssues, not from throws. The CLI returns exit 1 iff anyparseIssue.level === 'error'. The engine fails open — a bad schema does not throw, it logs. Your HTTP host must compute status frommanifest.parseIssues, not from atry/catcharoundtoArtifacts. resultsoutcomes:successmeans "the transform ran without throwing" — not "produced output".notSupportedis normal (a generator that doesn't apply to an item).skippedis filter exclusion. Diagnose "no output" via thefilesmap, notresults.- No history. The on-disk manifest is overwritten every run. If you want run history, persist each manifest yourself — it is already the right per-run telemetry payload.
Detail: concepts/the-manifest.md,
reference/manifest-format.md.
9. Provenance — the attribution / gen-maps subsystem
core/anchors/ is the provenance layer. Capture is always on;
emission is opt-in: pass attribution: { postPass: {...} } to
toArtifacts (the engine-level AttributionState has no enabled
field — the client.json#settings.anchors.enabled switch is the
user-facing toggle). When emission is on, the run produces two extra
artifact types alongside the code:
- Sidecar (
<file>.skm.json) — one per generated file. A pooled, position-indexed map: byte ranges in the rendered file → attribution tuples{ genId, srcPtr, variant, defName }, plus generator version and source registry. It is a source map for provenance — it answers "which generator, which schema location, which variant produced this span of code?".srcPtris a schema pointer likeoas:#/components/schemas/User. - Generation map (
_map.ndjson) — a project-level, per-Definition reverse-query index: "which files came from refNameUser?", "which files didgen-zodproduce?". Wholly rewritten each run.
Both live under .skmtc/<project>/.maps/ (gitignored by default).
Mechanism: SnippetBase instrumentation caches each producer's
rendered text; a post-pass walks the producer tree and AST-resolves
byte spans to landmarks. The AST parser (oxc-parser) does not
bundle into a Worker, so inside the Worker landmark names come
from Definition identifiers and a host-side post-pass can fill AST
detail later.
A lighter, always-on channel exists too: the manifest's previews
and mappings pair a per-Definition module with a source
descriptor ({ generatorId, operationPath, operationMethod } etc.)
— enough for a UI to say "this form was generated from POST /contacts".
If you are building provenance or "trace generated code back to
its schema" tooling, build on this subsystem (Sidecar,
GenerationMapEntry, AttributionState — exported from
@skmtc/core/Anchors). Do not reinvent it.
Full treatment — the four-stage mechanism, the Sidecar v2 format,
the worker-side parser omission, the doctor checks:
attribution-and-gen-maps.md.
10. The package graph and dependencies
The @skmtc/* packages
@skmtc/openapi-down-convert (OAS 3.1 → 3.0, vendored fork)
▲
@skmtc/convert (Swagger 2 / 3.1 → 3.0; YAML/JSON parse)
▲
@skmtc/core ◀───────────────── the engine; depended on by everything
▲ ▲ ▲ ▲
│ │ │ └── @skmtc/gen-* (generators; + peer generators)
│ │ └────── @skmtc/server (Hono HTTP host)
│ └────────── @skmtc/worker (Deno Worker host)
└────────────── @skmtc/cli ──▶ @skmtc/mcp (MCP host wraps the CLI)
cli also depends on convert and worker. Exact versions live in
each package's deno.json — treat that as canonical, not this skill.
The substrate
SKMTC's design principle "build on the substrate, don't rebuild it" means Deno is the platform:
deno bundleis the bundler — it compiles a project'sworker.tsintobundle.js.new Worker(...)withdeno.permissionsis the sandbox.- JSR is the package registry. Generators and packages are
ordinary JSR packages — there is no bespoke registry layer (that
was explicitly rejected). Two registries are in play:
jsr.io(public) andjsr.skmtc.dev(the SKMTC private registry). - The hosted Sandbox API runs on Deno Deploy (hence
DENO_DEPLOYMENT_ID/DENO_REGIONflowing into the manifest).
The project is Deno-locked, and that is an accepted trade. The generated output, however, runs anywhere TypeScript runs.
Key third-party dependencies
| Dependency | Used for |
|---|---|
valibot |
Runtime validation of the manifest, settings, parse issues, generator configs, and sidecars. Each schema is paired with a TS type via an unread _driftCheck binding — do not delete those. |
graphql |
GraphQL SDL parsing (Worker-side). |
oxc-parser |
AST parsing for the attribution post-pass (chosen over tsc because tsc won't bundle into a Worker). |
openapi-types |
Type definitions for OpenAPI documents. |
hono |
The @skmtc/server HTTP framework. |
@modelcontextprotocol/sdk |
The @skmtc/mcp server. |
swagger2openapi |
Swagger 2.0 → OpenAPI 3.0 conversion inside @skmtc/convert. |
@cliffy/command, ink, react |
CLI command parsing and terminal UI. |
Version-pin discipline: inter-package @skmtc/* dependencies are
pinned to exact JSR versions — no caret ranges — so a cloned
generator and the engine it compiles against can't silently skew.
skmtc doctor checks that a project's @skmtc/core pin matches the
CLI's. Pins can lag between packages; always read deno.json.
11. Building infrastructure around SKMTC
The user's context: a GitHub-like SaaS for hosting APIs and generators, running them, and supporting tracing and provenance. Here is how the engine's concepts map onto that platform — and where the engine stops and your platform code begins.
The integration map
| Platform concern | What the engine gives you | What you build |
|---|---|---|
| Hosting APIs (schemas) | A schema enters toArtifacts as SkmtcDocumentInput — { type:'oas', value: OpenAPIV3.Document } or { type:'gql', value: sdlString }. @skmtc/convert normalizes Swagger 2 / OAS 3.1 to 3.0 before the engine. |
Schema storage, versioning, ingest validation, the normalize-on-ingest-or-on-run decision. |
| Hosting generators | Generators are ordinary JSR packages (@skmtc/gen-*). jsr.skmtc.dev is already a JSR-compatible private registry. |
A registry UX; discovery, search, featured ranking; the publish pipeline. |
| Running generators | A run needs a bundle — worker.ts (templated from the import map) compiled to bundle.js. toArtifacts then executes it. Three reference hosts exist: local Worker (CLI), Hono POST /artifacts (@skmtc/server), the Worker message protocol (@skmtc/worker). |
The run service: validate → convert → toArtifacts → return/store { artifacts, manifest }. Bundle build & cache. Execution pooling if you need throughput. |
| Tracing | traceId / spanId / deploymentId / region already populate the manifest and StackTrail; the results tree is trace-addressable. |
Shipping them to an observability backend; cross-run dashboards. |
| Provenance | The attribution / gen-maps subsystem (§8): sidecars + generationMap, opt-in via attribution. |
Persisting and serving them; a viewer that maps generated code ↔ schema ↔ generator version. |
@skmtc/server is the seed of the SaaS
server/src/createServer.ts is ~150 lines: a Hono app with
POST /artifacts (validate a discriminated body → convert →
toArtifacts → { artifacts, manifest }), GET /generators, and
POST /to-v3-json. A production run service is this pattern with
auth, tenancy, persistence, and bundle management added around it.
The MCP host already calls a deployed instance of this server.
Where the engine stops — build these at the platform layer
The engine deliberately does schema in → artifacts out and nothing else. It has no notion of:
- Users, identity, auth, multi-tenancy. A SKMTC "project" is a generator configuration, not a tenant. Tenancy is entirely yours to build.
- Persistence or run history.
toArtifactsreturns a value; the on-diskmanifest.jsonis overwritten every run. - Result caching. Determinism makes this safe and easy — cache
{ artifacts, manifest }by a hash of(schema, settings, bundle)— but it is your layer, never the engine's. - A warm execution pool, rate limiting, quotas, streaming. Fact 5: one cold Worker per run. Pool at the container/process level if you need throughput; never share a context across runs.
- A plugin / hook API. There isn't one and it is a rejected design. Extension happens by cloning generators or by writing a new host — not by hooking the engine.
12. Counter-intuitive facts for infrastructure builders
Beyond the five facts in §1. Left column = a reflex from generic backend / platform training data; right column = SKMTC reality.
| Infra reflex | SKMTC reality |
|---|---|
| Keep a warm worker pool for throughput | One Worker per run, spawned cold, terminated after RESULT. Determinism depends on fresh contexts. Pool containers, not engine contexts. |
| Do incremental builds — only regenerate changed operations | There are none. Every run is whole-document and from cold. Cache the whole result by input hash instead. |
| Retry a failed run | Runs are deterministic — a retry reproduces the same failure exactly. Fix the input; don't retry. |
Wrap toArtifacts in try/catch and 500 on throw |
It fails open — a bad schema logs a ParseIssue, it does not throw. Derive status from manifest.parseIssues, not from exceptions. |
| Stream artifacts for large outputs | The Worker batch-postMessages the whole result. No streaming protocol; structured clone handles typical sizes. |
| Format the output before returning it | Output is unformatted by design. Formatting is the consumer's separate step. |
| Let generators fetch schemas / templates at run time | The Worker has net: false. Everything a generator needs must already be in its inputs. Fetching is host-side, pre-engine. |
The on-disk manifest.json is the run record |
It is overwritten every run. Persist each manifest yourself for history. |
| Add an engine plugin/hook API for the platform | Rejected design. Extend by cloning generators or adding a host. |
| A "project" is a tenant / a customer | A project is a generator configuration. It is not a unit of tenancy, identity, or billing. |
| Generated code needs the SKMTC runtime at deploy time | Zero runtime. Output is plain committed source; the engine never ships to the consumer's bundle. |
13. Boundaries with other skills
- skmtc-generator — authoring and editing generators (Projections, Snippets, the DSL, customization seams). Load when writing generator code.
- skmtc-cli — running the CLI, configuring
client.json, enrichments, skip/include. Load when operating SKMTC. - skmtc-debug — diagnosing broken runs (no output, wrong output, errors). Verify-first stance. Load when something is broken.
- skmtc-retro — end-of-session reflection / friction capture.
- This skill (skmtc-architecture) — the system mental model for reasoning about and building infrastructure around the engine.
If the question is how the system works or how to build a service around it, this skill. If it is how to write a generator, how to run a command, or why a run is broken, hand off.
14. Cross-references
Concepts —
the-three-phases.md ·
the-worker-runtime.md ·
the-manifest.md ·
stack-trail.md (reference) ·
cross-generator-coordination.md ·
generators-as-packages.md ·
attribution-and-gen-maps.md
Explanation —
design-philosophy.md ·
security-model.md ·
comparison-to-other-tools.md ·
status-and-roadmap.md
Reference —
glossary.md ·
manifest-format.md ·
llms.md (consolidated operational reference)
Source landmarks — core/run/toArtifacts.ts (engine entry) ·
worker/mod.ts (Worker host) · server/src/createServer.ts (HTTP
host) · core/context/ (the three context classes) · core/anchors/
(attribution / gen-maps).