Install
$ agentstack add skill-skmtc-skmtc-skmtc-model ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
Model generators: fill the skeleton
A model generator turns each component schema (refName) into one definition in one file: entry → projection → schema-type router → one snippet class per schema type. That structure is invariant across target libraries — only naming policy and per-type syntax vary. So do not write the structure: copy it.
1. The method
The skeleton/ directory next to this file is a complete, compiling, engine-tested model generator that renders a placeholder syntax (m.object({...})). Author by transplant, not from scratch:
- Copy
skeleton/to your package location; run
deno test --allow-env --allow-sys --allow-read — 6 green tests prove the machinery before you touch anything.
- Rename:
nameindeno.json;MyLib→YourLibin class
names and filenames; myLibEntry → yourLibEntry; then src/lib.ts — LIB_MODULE (emitted module specifier) and LIB (imported symbol).
- Fill the slots (§2), smallest first: scalars → string/enum →
array/object → union → lazy/recursion annotation.
- Re-pin the test: update the pinned strings in
mod.test.tsto
your target syntax. The structural assertions (files exist, shared refs dedup to ONE definition, import headers stitched, recursion annotated) must pass UNCHANGED — if one breaks, you broke machinery, not syntax.
Every slot is a // SLOT(name): comment. Everything outside a SLOT is engine machinery — modifying it is almost always a mistake.
2. The slots
| Slot | File | Decision | |---|---|---| | library | src/lib.ts | emitted module + symbol, single point | | naming, identifier-kind, export-path | src/base.ts | identity policy (from refName ONLY) | | string, string-constraints | src/MyLibString.ts | string / enum / literal syntax; formats, min/maxLength | | number, integer, boolean, unknown, void | src/MyLibScalars.ts | scalar syntax; numeric constraints | | array | src/MyLibArray.ts | list syntax | | object-properties, object-intersection, object-empty, visibility | src/MyLibObject.ts | object syntax; properties+record composition; readOnly/writeOnly policy | | record | src/MyLibObject.ts | additionalProperties map syntax | | union | src/MyLibUnion.ts | oneOf/anyOf; discriminated form | | lazy | src/MyLibRef.ts | deferred-reference form for cycles | | recursion-annotation | src/MyLibProjection.ts | type annotation breaking circular inference | | modifiers | src/modifiers.ts | optional/nullable syntax and wrap order | | enrichments | src/enrichments.ts | config seam (default: opt-out) |
3. Edge cases the skeleton already handles — keep them working
- Refs are names, never expansions.
MyLibRefputs only the peer's
NAME in the value tree; the ModelDriver resolves the definition (cache hit → reuse, miss → construct) and stitches the cross-file import. Inline-expanding a ref, or hand-writing its import, is how shared models duplicate.
- Recursion is a protocol, not a special case. A back-reference to
a model still open on the build stack (context.modelDepth > 0) renders via SLOT(lazy) and bumps the depth; the projection then sees > 1 and sets settings.identifier.typeName (SLOT recursion-annotation) so the emitted export const doesn't die of circular inference (TS7022/7024). Self-recursion only — mutual recursion is not detected.
- Optional and nullable are different axes.
requiredcomes from
the PARENT object's required list and flows into each property leaf's modifiers; nullable sits on the node itself. Both render exactly once, in applyModifiers, at the leaf — no other owner, and never while building stored fields.
- additionalProperties → the record path;
true/empty schema →
the unknown fallback; properties + additionalProperties together → SLOT(object-intersection).
- **An object schema has four forms — and position can change the
render.** Properties-only, record-only (additionalProperties), both, empty: every place an object renders must survive all four. In TypeScript one expression serves both type and declaration positions (z.object({...}), .and(z.record(...)) for both-forms), so the object SLOTs compose freely. In a head+value language (Kotlin) the two positions DIVERGE, and a position-blind toString() cannot serve both (compiler-verified 2026-08-04, kotlin-debug rig): properties-only declares as a data class parameter list, and in type position must render a NAME — synthesize the named sibling declaration and reference it (name derived from the schema's own stackTrail, no naming param threaded through the router; collisions policed by a document-wide claim registry that throws per-item, since the name shares a PACKAGE with every component class — gen-kotlin-jackson toSynthesizedName.ts + synthesizedNames.ts; a parameter list in type position parses as a function type and fails, and widening to Map discards the type — capitulation, not a solution); record-only and empty must not take a data-class head at all (data class X() is illegal — their declaration kind is typealias); both-forms has a declaration form (data class plus a @field:JsonAnySetter @get:JsonAnyGetter catch-all map property) but no anonymous type form. Decide the identifier KIND and the value together from the same schema guards (gen-kotlin-jackson shape.ts is the worked example) — never from the name alone, and never by making one toString() answer both positions.
- A discriminated union may be a DECLARATION, not an expression.
In TypeScript SLOT(union) is one expression (z.discriminatedUnion("type", [...])). In a language without union types (Kotlin) a qualifying discriminated union becomes a named sealed declaration, and the member models must declare the supertype — a member may be BUILT before its union is ever seen, so membership comes from a document-wide scan (parent → member inversion, WeakMap-memoized) consulted at member construction, never from the union's own walk. Non-qualifying unions render the honest wire type (JsonNode), not Any. Full pattern: the Kotlin lang skill §8c.
- Property keys go through
handleKey—'first-name'renders
quoted; never assume keys are identifiers.
- Visibility.
readOnly/writeOnlyare captured per property in
MyLibObjectProperties.visibility. Default policy ignores them; if the target needs them, annotate the value (e.g. .readonly()) or emit request/response variants via variant threading — decide at SLOT(visibility).
- Unknown never throws. Untyped schemas route to the unknown
fallback so one odd schema can't kill the subject. custom values pass through untouched.
- TypeSystem contracts. Each snippet class carries the fields peers
rely on (TypeSystemString needs format + enums; objects expose objectProperties/recordProperties). Add fields freely; remove none — removal breaks insertNormalizedModel consumers and fails the SchemaToValueFn check.
4. Verify
The shipped mod.test.ts runs the REAL pipeline (toArtifacts) over a fixture with an enum, an array-of-ref, a shared ref (×2 → one definition), optional + nullable, a record, and a self-recursive model. It is your regression gate: green before you start, green after every slot. Read failures in this order: import header first (a missing import means a string swallowed a snippet), then the body, then deno lint (the skmtc/* rules are wired in deno.json).
5. Model-generator pitfalls
| Symptom | Fix | |---|---| | Shared model duplicated per consumer | A ref was rendered/expanded instead of flowing through MyLibRef | | Stack overflow on recursive schema | The modelDepth branch in MyLibRef was removed or bypassed | | Emitted file dies of TS7022/7024 | SLOT(recursion-annotation) not set for the target | | .optional() doubled or missing | Modifiers applied outside applyModifiers, or a second owner added | | Enum with null member renders 'null' | Keep the literal() null-guard from MyLibString | | Peer generator can't consume yours | schemaToValueFn/createIdentifier statics or TypeSystem contract fields removed | | Lint fires no-template-imports/no-adhoc-tostring | Target syntax leaked outside a toString() body — move it into the SLOT | | data class NameMap (head glued to a type) in output | Declaration kind and value were decided separately — see the four-forms bullet in §3; kind+value must come from the same schema guards |
6. Boundaries
Engine semantics (the one law, memoization, enrichments, variants, naming rules) live in skmtc-generator — read it first. TS-layer specifics (register shapes, identifier kinds, import machinery, List/FunctionParameter) live in skmtc-lang-typescript. This skill owns only the model SHAPE. The skeleton is TypeScript-emitting; for a Kotlin model generator, keep this skill's shape and edge-case rules but take call shapes from the Kotlin lang skill (no Kotlin skeleton yet). Operation generators are a different shape — load skmtc-operation; accumulators are covered by neither (clone gen-msw/gen-express per skmtc-generator §2).
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: skmtc
- Source: skmtc/skmtc
- License: Apache-2.0
- Homepage: https://skm.tc
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.