From 76d7d0ec4e1a4eacb0b34d134b03365d34f433da Mon Sep 17 00:00:00 2001 From: tkoehlerlg Date: Sun, 15 Mar 2026 13:42:38 +0100 Subject: [PATCH 1/4] feat: add @tanstack/intent skills for AI agent guidance Co-Authored-By: Claude Opus 4.6 --- bin/intent.js | 20 ++ bun.lock | 5 + package.json | 178 ++++++++------- skills/_artifacts/domain_map.yaml | 318 +++++++++++++++++++++++++++ skills/_artifacts/skill_spec.md | 98 +++++++++ skills/_artifacts/skill_tree.yaml | 78 +++++++ skills/build-transformer/SKILL.md | 266 ++++++++++++++++++++++ skills/elysia-integration/SKILL.md | 167 ++++++++++++++ skills/hono-integration/SKILL.md | 171 ++++++++++++++ skills/production-readiness/SKILL.md | 215 ++++++++++++++++++ 10 files changed, 1434 insertions(+), 82 deletions(-) create mode 100644 bin/intent.js create mode 100644 skills/_artifacts/domain_map.yaml create mode 100644 skills/_artifacts/skill_spec.md create mode 100644 skills/_artifacts/skill_tree.yaml create mode 100644 skills/build-transformer/SKILL.md create mode 100644 skills/elysia-integration/SKILL.md create mode 100644 skills/hono-integration/SKILL.md create mode 100644 skills/production-readiness/SKILL.md diff --git a/bin/intent.js b/bin/intent.js new file mode 100644 index 0000000..2cf2efa --- /dev/null +++ b/bin/intent.js @@ -0,0 +1,20 @@ +#!/usr/bin/env node +// Auto-generated by @tanstack/intent setup +// Exposes the intent end-user CLI for consumers of this library. +// Commit this file, then add to your package.json: +// "bin": { "intent": "./bin/intent.js" } +try { + await import('@tanstack/intent/intent-library') +} catch (e) { + if (e?.code === 'ERR_MODULE_NOT_FOUND' || e?.code === 'MODULE_NOT_FOUND') { + console.error('@tanstack/intent is not installed.') + console.error('') + console.error('Install it as a dev dependency:') + console.error(' npm add -D @tanstack/intent') + console.error('') + console.error('Or run directly:') + console.error(' npx @tanstack/intent@latest list') + process.exit(1) + } + throw e +} diff --git a/bun.lock b/bun.lock index 4bb79dd..9ead12c 100644 --- a/bun.lock +++ b/bun.lock @@ -6,6 +6,7 @@ "name": "t7m", "devDependencies": { "@biomejs/biome": "2.4.6", + "@tanstack/intent": "^0.0.19", "@types/bun": "^1.2.17", "elysia": "^1.2", "hono": "^4", @@ -44,6 +45,8 @@ "@sinclair/typebox": ["@sinclair/typebox@0.34.48", "", {}, "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA=="], + "@tanstack/intent": ["@tanstack/intent@0.0.19", "", { "dependencies": { "yaml": "^2.7.0" }, "bin": { "intent": "dist/cli.mjs", "intent-library": "dist/intent-library.mjs" } }, "sha512-i2Tt+hxOY9zPBJpgZK0Fyi8bVljQbj907ZRivaTwwHLyS4V8j9Gk4nLjBMYtMP22djUmExJ+P4G35bVIZ6+xHA=="], + "@tokenizer/inflate": ["@tokenizer/inflate@0.4.1", "", { "dependencies": { "debug": "^4.4.3", "token-types": "^6.1.1" } }, "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA=="], "@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="], @@ -85,5 +88,7 @@ "uint8array-extras": ["uint8array-extras@1.5.0", "", {}, "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A=="], "undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="], + + "yaml": ["yaml@2.8.2", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A=="], } } diff --git a/package.json b/package.json index c51145b..f6d1a30 100644 --- a/package.json +++ b/package.json @@ -1,84 +1,98 @@ { - "name": "t7m", - "module": "dist/index.js", - "type": "module", - "author": { - "name": "Torben Koehler" - }, - "license": "MIT-NSR", - "version": "1.0.0", - "description": "Transformer for Elysia and Hono", - "repository": { - "type": "git", - "url": "git+https://github.com/tkoehlerlg/t7m.git" - }, - "bugs": { - "url": "https://github.com/tkoehlerlg/t7m/issues" - }, - "homepage": "https://github.com/tkoehlerlg/t7m#readme", - "exports": { - ".": { - "import": "./dist/index.js", - "types": "./dist/index.d.ts" - }, - "./hono": { - "import": "./dist/hono/index.js", - "types": "./dist/hono/index.d.ts" - }, - "./elysia": { - "import": "./dist/elysia/index.js", - "types": "./dist/elysia/index.d.ts" - }, - "./*": null - }, - "typesVersions": { - "*": { - "hono": ["./dist/hono/index.d.ts"], - "elysia": ["./dist/elysia/index.d.ts"] - } - }, - "types": "dist/index.d.ts", - "files": ["dist"], - "scripts": { - "build": "rm -rf dist && tsc --declaration --emitDeclarationOnly --outDir dist && NODE_ENV=production bun build src/index.ts src/hono/index.ts src/elysia/index.ts --outdir dist --target node --external hono --external elysia", - "dev": "bun --watch src/index.ts", - "format": "biome format --write", - "lint": "biome lint && biome check", - "lint:fix": "biome lint --fix && biome check --fix", - "lint:fix:unsafe": "biome lint --fix --unsafe && biome check --fix", - "typecheck": "tsc --noEmit", - "test": "bun test", - "test:coverage": "bun test --coverage --coverage-reporter=lcov --coverage-reporter=text", - "prepublishOnly": "bun format && NODE_ENV=production bun run build && bun run test" - }, - "devDependencies": { - "@biomejs/biome": "2.4.6", - "@types/bun": "^1.2.17", - "elysia": "^1.2", - "hono": "^4" - }, - "peerDependencies": { - "typescript": "^5", - "elysia": "^1.2", - "hono": "^4" - }, - "peerDependenciesMeta": { - "elysia": { - "optional": true - }, - "hono": { - "optional": true - } - }, - "keywords": [ - "elysia", - "hono", - "transformer", - "typescript", - "web-framework", - "api", - "middleware", - "type-safe", - "route-handler" - ] + "name": "t7m", + "module": "dist/index.js", + "type": "module", + "author": { + "name": "Torben Koehler" + }, + "license": "MIT-NSR", + "version": "1.0.0", + "description": "Transformer for Elysia and Hono", + "repository": { + "type": "git", + "url": "git+https://github.com/tkoehlerlg/t7m.git" + }, + "bugs": { + "url": "https://github.com/tkoehlerlg/t7m/issues" + }, + "homepage": "https://github.com/tkoehlerlg/t7m#readme", + "exports": { + ".": { + "import": "./dist/index.js", + "types": "./dist/index.d.ts" + }, + "./hono": { + "import": "./dist/hono/index.js", + "types": "./dist/hono/index.d.ts" + }, + "./elysia": { + "import": "./dist/elysia/index.js", + "types": "./dist/elysia/index.d.ts" + }, + "./*": null + }, + "typesVersions": { + "*": { + "hono": [ + "./dist/hono/index.d.ts" + ], + "elysia": [ + "./dist/elysia/index.d.ts" + ] + } + }, + "types": "dist/index.d.ts", + "files": [ + "dist", + "skills", + "bin", + "!skills/_artifacts" + ], + "scripts": { + "build": "rm -rf dist && tsc --declaration --emitDeclarationOnly --outDir dist && NODE_ENV=production bun build src/index.ts src/hono/index.ts src/elysia/index.ts --outdir dist --target node --external hono --external elysia", + "dev": "bun --watch src/index.ts", + "format": "biome format --write", + "lint": "biome lint && biome check", + "lint:fix": "biome lint --fix && biome check --fix", + "lint:fix:unsafe": "biome lint --fix --unsafe && biome check --fix", + "typecheck": "tsc --noEmit", + "test": "bun test", + "test:coverage": "bun test --coverage --coverage-reporter=lcov --coverage-reporter=text", + "prepublishOnly": "bun format && NODE_ENV=production bun run build && bun run test" + }, + "devDependencies": { + "@biomejs/biome": "2.4.6", + "@tanstack/intent": "^0.0.19", + "@types/bun": "^1.2.17", + "elysia": "^1.2", + "hono": "^4" + }, + "peerDependencies": { + "typescript": "^5", + "elysia": "^1.2", + "hono": "^4" + }, + "peerDependenciesMeta": { + "elysia": { + "optional": true + }, + "hono": { + "optional": true + } + }, + "keywords": [ + "elysia", + "hono", + "transformer", + "typescript", + "web-framework", + "api", + "middleware", + "type-safe", + "route-handler", + "tanstack-intent" + ], + "bin": { + "intent": "./bin/intent.js" + } } diff --git a/skills/_artifacts/domain_map.yaml b/skills/_artifacts/domain_map.yaml new file mode 100644 index 0000000..7b820cb --- /dev/null +++ b/skills/_artifacts/domain_map.yaml @@ -0,0 +1,318 @@ +# domain_map.yaml +# Generated by skill-domain-discovery +# Library: t7m +# Version: 1.0.0 +# Date: 2026-03-15 +# Status: reviewed + +library: + name: 't7m' + version: '1.0.0' + repository: 'https://github.com/tkoehlerlg/t7m' + description: 'TypeScript library for API output transformation with type-safe includes, caching, and concurrency control' + primary_framework: 'framework-agnostic' + +domains: + - name: 'framework integration' + slug: 'framework-integration' + description: 'Setting up t7m in a Hono or Elysia application — middleware/plugin wiring, query param parsing, response shaping' + + - name: 'transformer authoring' + slug: 'transformer-authoring' + description: 'Building transformers — data mapping, includes, props, caching, nested transformers, concurrency control' + + - name: 'production operations' + slug: 'production-ops' + description: 'Tuning t7m for production — concurrency limits, cache strategy, Cloudflare constraints' + +skills: + - name: 'Hono Integration' + slug: 'hono-integration' + domain: 'framework-integration' + description: 'Set up t7mMiddleware in a Hono app with typed transforms, query-based includes, wrappers, and custom status codes' + type: 'framework' + covers: + - 't7mMiddleware' + - 'c.transform()' + - 'c.transformMany()' + - 'extras object (includes, wrapper, debug, props)' + - 'query param parsing (?include=)' + - 'status codes and headers' + tasks: + - 'Wire t7mMiddleware into a Hono app' + - 'Return transformed data from a route handler' + - 'Support query-based includes on an endpoint' + - 'Wrap transformed output in a response envelope' + - 'Set custom status codes and headers on transform responses' + failure_modes: + - mistake: 'Calling transformer.transform() instead of c.transform()' + mechanism: 'Middleware injects transform on context — calling the transformer directly bypasses query param parsing and response typing' + wrong_pattern: | + app.get('/users/:id', async (c) => { + const user = await getUser(c.req.param('id')) + return c.json(await userTransformer.transform({ input: user })) + }) + correct_pattern: | + app.get('/users/:id', async (c) => { + const user = await getUser(c.req.param('id')) + return c.transform(user, userTransformer) + }) + source: 'src/hono/middleware.ts' + priority: 'CRITICAL' + status: 'active' + + - mistake: 'Passing props as third argument instead of in extras' + mechanism: 'c.transform takes (input, transformer, extras) — props go inside the extras object, not as a separate argument' + wrong_pattern: | + return c.transform(user, userTransformer, { db }) + correct_pattern: | + return c.transform(user, userTransformer, { props: { db } }) + source: 'src/hono/middleware.ts' + priority: 'HIGH' + status: 'active' + + - mistake: 'Not registering middleware before routes' + mechanism: 't7mMiddleware must be registered with app.use() before route handlers that call c.transform()' + wrong_pattern: | + app.get('/users', async (c) => c.transform(user, transformer)) + app.use(t7mMiddleware) + correct_pattern: | + app.use(t7mMiddleware) + app.get('/users', async (c) => c.transform(user, transformer)) + source: 'src/hono/middleware.ts' + priority: 'CRITICAL' + status: 'active' + + - mistake: 'Returning c.transform result inside c.json()' + mechanism: 'c.transform() already returns a typed JSON response — wrapping it in c.json() double-serializes' + wrong_pattern: | + return c.json(await c.transform(user, transformer)) + correct_pattern: | + return c.transform(user, transformer) + source: 'src/hono/middleware.ts' + priority: 'HIGH' + status: 'active' + + - name: 'Elysia Integration' + slug: 'elysia-integration' + domain: 'framework-integration' + description: 'Set up t7mPlugin in an Elysia app with typed transforms, query-based includes, and plain data returns' + type: 'framework' + covers: + - 't7mPlugin' + - 'transform()' + - 'transformMany()' + - 'extras object (includes, wrapper, debug, props)' + - 'query param parsing (?include=)' + tasks: + - 'Wire t7mPlugin into an Elysia app' + - 'Return transformed data from a route handler' + - 'Support query-based includes on an endpoint' + - 'Wrap transformed output in a response envelope' + failure_modes: + - mistake: 'Expecting a Response object like Hono' + mechanism: 'Elysia transform returns plain data, not a Response — Elysia handles serialization' + wrong_pattern: | + app.get('/users/:id', async ({ transform, params }) => { + const user = await getUser(params.id) + const response = await transform(user, transformer) + return new Response(JSON.stringify(response)) + }) + correct_pattern: | + app.get('/users/:id', async ({ transform, params }) => { + const user = await getUser(params.id) + return transform(user, transformer) + }) + source: 'src/elysia/plugin.ts' + priority: 'HIGH' + status: 'active' + + - mistake: 'Passing extras when no props needed' + mechanism: 'In Elysia, extras is optional when transformer has no Props type — agents often pass an empty object anyway' + wrong_pattern: | + return transform(user, transformer, {}) + correct_pattern: | + return transform(user, transformer) + source: 'src/elysia/types.ts' + priority: 'MEDIUM' + status: 'active' + + - mistake: 'Trying to set status codes in transform return' + mechanism: 'Unlike Hono, Elysia transform has no status parameter — use Elysia set.status instead' + wrong_pattern: | + return transform(user, transformer, { status: 201 }) + correct_pattern: | + set.status = 201 + return transform(user, transformer) + source: 'src/elysia/plugin.ts' + priority: 'HIGH' + status: 'active' + + - name: 'Build Transformer' + slug: 'build-transformer' + domain: 'transformer-authoring' + description: 'Create transformers with data(), includesMap, props, cache, nested transformers, and concurrency control' + type: 'core' + covers: + - 'AbstractTransformer' + - 'data() method' + - 'includesMap' + - 'includes (type-safe and unsafe)' + - 'props (conditional requirement)' + - 'Cache with .call()' + - 'transformers record' + - 'clearCacheOnTransform' + - 'concurrency and includesConcurrency' + - 'InputOf, OutputOf, PropsOf, IncludesOf' + tasks: + - 'Create a basic transformer mapping input to output' + - 'Add optional includes to a transformer' + - 'Add required props to a transformer' + - 'Set up caching on a transformer' + - 'Nest transformers for related data' + - 'Configure concurrency limits for batch transforms' + failure_modes: + - mistake: 'Passing positional args instead of object' + mechanism: 'All transform methods take a single params object — not positional arguments' + wrong_pattern: | + await transformer.transform(user) + await transformer.transform(user, { includes: ['posts'] }) + correct_pattern: | + await transformer.transform({ input: user }) + await transformer.transform({ input: user, includes: ['posts'] }) + source: 'CLAUDE.md gotcha #2' + priority: 'CRITICAL' + status: 'active' + + - mistake: 'Using cache.get() instead of cache.call()' + mechanism: 'Cache exposes .call() to invoke the cached function, not .get()' + wrong_pattern: | + const user = await this.cache.userProfile.get(input.userId) + correct_pattern: | + const user = await this.cache.userProfile.call(input.userId) + source: 'CLAUDE.md gotcha #3' + priority: 'CRITICAL' + status: 'active' + + - mistake: 'Putting required properties in includesMap' + mechanism: 'Includes only work with optional output properties — enforced by OnlyPossiblyUndefined' + wrong_pattern: | + type Output = { name: string; avatar: string } + includesMap = { avatar: async () => '...' } // avatar is required + correct_pattern: | + type Output = { name: string; avatar?: string } + includesMap = { avatar: async () => '...' } // avatar is optional + source: 'CLAUDE.md gotcha #7' + priority: 'CRITICAL' + status: 'active' + + - mistake: 'Passing clearCacheOnTransform to Cache constructor' + mechanism: 'clearCacheOnTransform is an AbstractTransformer constructor option, not a Cache option' + wrong_pattern: | + new Cache(fn, { clearCacheOnTransform: false }) + correct_pattern: | + constructor() { super({ clearCacheOnTransform: false }) } + source: 'CLAUDE.md gotcha #5' + priority: 'HIGH' + status: 'active' + + - mistake: 'Using transformers as an array' + mechanism: 'transformers is a Record AnyAbstractTransformer>>, not an array' + wrong_pattern: | + transformers = [this.authorTransformer] + correct_pattern: | + transformers = { author: this.authorTransformer } + source: 'CLAUDE.md gotcha #4' + priority: 'HIGH' + status: 'active' + + - mistake: 'Setting includesConcurrency in constructor' + mechanism: 'includesConcurrency is a class property like includesMap, not a constructor parameter' + wrong_pattern: | + constructor() { super({ includesConcurrency: { posts: 3 } }) } + correct_pattern: | + includesConcurrency = { posts: 3 } + source: 'CLAUDE.md gotcha #10' + priority: 'HIGH' + status: 'active' + + - name: 'Production Readiness' + slug: 'production-readiness' + domain: 'production-ops' + description: 'Configure concurrency limits, caching strategy, and Cloudflare-specific constraints for production t7m deployments' + type: 'lifecycle' + covers: + - 'concurrency constructor option' + - 'includesConcurrency class property' + - 'Semaphore' + - 'clearCacheOnTransform' + - 'Cache maxSize' + - 'Cloudflare Workers subrequest limits' + tasks: + - 'Set concurrency limits for batch transforms' + - 'Configure per-include concurrency for external API calls' + - 'Tune cache behavior for high-throughput endpoints' + - 'Deploy t7m to Cloudflare Workers within subrequest limits' + failure_modes: + - mistake: 'No concurrency limit on Cloudflare Workers' + mechanism: 'Cloudflare Workers have a 50 subrequest limit per invocation — unbounded transformMany with external calls will hit this' + wrong_pattern: | + class UserTransformer extends AbstractTransformer { + constructor() { super() } + } + correct_pattern: | + class UserTransformer extends AbstractTransformer { + constructor() { super({ concurrency: 10 }) } + } + source: 'maintainer interview' + priority: 'CRITICAL' + status: 'active' + + - mistake: 'Expecting concurrency to limit single transform calls' + mechanism: 'concurrency only throttles transformMany/transformMany — single transform() calls are not limited by it' + wrong_pattern: | + // Assuming concurrency: 5 limits parallel transform() calls + await Promise.all(users.map(u => transformer.transform({ input: u }))) + correct_pattern: | + // Use transformMany for batch operations with concurrency control + await transformer.transformMany({ inputs: users }) + source: 'CLAUDE.md gotcha #9' + priority: 'HIGH' + status: 'active' + + - mistake: 'Unbounded cache without maxSize' + mechanism: 'Cache grows without limit by default — high-cardinality keys cause memory issues' + wrong_pattern: | + cache = { userProfile: new Cache((id: string) => fetchUser(id)) } + correct_pattern: | + cache = { userProfile: new Cache((id: string) => fetchUser(id), { maxSize: 100 }) } + source: 'src/lib/cache.ts' + priority: 'HIGH' + status: 'active' + +tensions: + - name: 'Simplicity vs production safety' + skills: ['build-transformer', 'production-readiness'] + description: 'Default transformer config (no concurrency limits, clearCacheOnTransform: true) works for development but can cause issues under load or on Cloudflare' + implication: 'Agent generates working transformers with zero production config — looks correct in dev, breaks under real traffic or Cloudflare subrequest limits' + + - name: 'Type safety vs flexibility' + skills: ['build-transformer', 'hono-integration'] + description: 'Strict typing (includes only on optional props, conditional props requirement) prevents misuse but can confuse agents into adding unnecessary type workarounds' + implication: 'Agent adds type assertions or any casts to bypass constraints instead of restructuring the output type correctly' + +cross_references: + - from: 'hono-integration' + to: 'build-transformer' + reason: 'Framework setup requires understanding transformer structure to pass correct extras (props, includes)' + - from: 'elysia-integration' + to: 'build-transformer' + reason: 'Same as Hono — transformer knowledge required to wire up routes correctly' + - from: 'build-transformer' + to: 'production-readiness' + reason: 'Transformers need production tuning (concurrency, cache) before deployment' + - from: 'hono-integration' + to: 'elysia-integration' + reason: 'Developers pick one framework — understanding differences prevents cross-contamination of patterns' + +gaps: [] diff --git a/skills/_artifacts/skill_spec.md b/skills/_artifacts/skill_spec.md new file mode 100644 index 0000000..4efb38c --- /dev/null +++ b/skills/_artifacts/skill_spec.md @@ -0,0 +1,98 @@ +# t7m — Skill Spec + +t7m is a TypeScript library for transforming API output with type-safe includes, caching, and concurrency control. It ships as a framework-agnostic core with optional Hono middleware and Elysia plugin integrations. + +## Domains + +| Domain | Description | Skills | +| --- | --- | --- | +| framework-integration | Setting up t7m in a Hono or Elysia app | hono-integration, elysia-integration | +| transformer-authoring | Building transformers with data, includes, props, cache | build-transformer | +| production-ops | Tuning for production — concurrency, caching, Cloudflare | production-readiness | + +## Skill Inventory + +| Skill | Type | Domain | What it covers | Failure modes | +| --- | --- | --- | --- | --- | +| hono-integration | framework | framework-integration | t7mMiddleware, c.transform(), extras, query params, status codes | 4 | +| elysia-integration | framework | framework-integration | t7mPlugin, transform(), extras, query params, plain data return | 3 | +| build-transformer | core | transformer-authoring | AbstractTransformer, data(), includes, props, cache, nested transformers, concurrency | 6 | +| production-readiness | lifecycle | production-ops | concurrency limits, cache maxSize, clearCacheOnTransform, Cloudflare constraints | 3 | + +## Failure Mode Inventory + +### Hono Integration (4 failure modes) + +| # | Mistake | Priority | Source | Cross-skill? | +| --- | --- | --- | --- | --- | +| 1 | Calling transformer.transform() instead of c.transform() | CRITICAL | src/hono/middleware.ts | — | +| 2 | Passing props as third argument instead of in extras | HIGH | src/hono/middleware.ts | — | +| 3 | Not registering middleware before routes | CRITICAL | src/hono/middleware.ts | — | +| 4 | Returning c.transform result inside c.json() | HIGH | src/hono/middleware.ts | — | + +### Elysia Integration (3 failure modes) + +| # | Mistake | Priority | Source | Cross-skill? | +| --- | --- | --- | --- | --- | +| 1 | Expecting a Response object like Hono | HIGH | src/elysia/plugin.ts | — | +| 2 | Passing extras when no props needed | MEDIUM | src/elysia/types.ts | — | +| 3 | Trying to set status codes in transform return | HIGH | src/elysia/plugin.ts | — | + +### Build Transformer (6 failure modes) + +| # | Mistake | Priority | Source | Cross-skill? | +| --- | --- | --- | --- | --- | +| 1 | Passing positional args instead of object | CRITICAL | CLAUDE.md | — | +| 2 | Using cache.get() instead of cache.call() | CRITICAL | CLAUDE.md | — | +| 3 | Putting required properties in includesMap | CRITICAL | CLAUDE.md | — | +| 4 | Passing clearCacheOnTransform to Cache constructor | HIGH | CLAUDE.md | — | +| 5 | Using transformers as an array | HIGH | CLAUDE.md | — | +| 6 | Setting includesConcurrency in constructor | HIGH | CLAUDE.md | — | + +### Production Readiness (3 failure modes) + +| # | Mistake | Priority | Source | Cross-skill? | +| --- | --- | --- | --- | --- | +| 1 | No concurrency limit on Cloudflare Workers | CRITICAL | maintainer interview | — | +| 2 | Expecting concurrency to limit single transform calls | HIGH | CLAUDE.md | — | +| 3 | Unbounded cache without maxSize | HIGH | src/lib/cache.ts | — | + +## Tensions + +| Tension | Skills | Agent implication | +| --- | --- | --- | +| Simplicity vs production safety | build-transformer ↔ production-readiness | Agent generates working transformers with zero production config | +| Type safety vs flexibility | build-transformer ↔ hono-integration | Agent adds type casts to bypass constraints instead of restructuring | + +## Cross-References + +| From | To | Reason | +| --- | --- | --- | +| hono-integration | build-transformer | Framework setup requires understanding transformer structure | +| elysia-integration | build-transformer | Same — transformer knowledge needed for route wiring | +| build-transformer | production-readiness | Transformers need production tuning before deployment | +| hono-integration | elysia-integration | Understanding differences prevents cross-contamination | + +## Subsystems & Reference Candidates + +| Skill | Subsystems | Reference candidates | +| --- | --- | --- | +| hono-integration | — | — | +| elysia-integration | — | — | +| build-transformer | — | — | +| production-readiness | — | — | + +## Recommended Skill File Structure + +- **Core skills:** build-transformer +- **Framework skills:** hono-integration, elysia-integration (each has `requires: [build-transformer]`) +- **Lifecycle skills:** production-readiness +- **Composition skills:** none needed (Hono and Elysia are peer deps, not companions) +- **Reference files:** none needed (API surface is small) + +## Composition Opportunities + +| Library | Integration points | Composition skill needed? | +| --- | --- | --- | +| Hono | t7mMiddleware, c.transform() | No — covered by hono-integration skill | +| Elysia | t7mPlugin, transform() | No — covered by elysia-integration skill | diff --git a/skills/_artifacts/skill_tree.yaml b/skills/_artifacts/skill_tree.yaml new file mode 100644 index 0000000..477e56d --- /dev/null +++ b/skills/_artifacts/skill_tree.yaml @@ -0,0 +1,78 @@ +# skills/_artifacts/skill_tree.yaml +library: + name: 't7m' + version: '1.0.0' + repository: 'https://github.com/tkoehlerlg/t7m' + description: 'TypeScript library for API output transformation with type-safe includes, caching, and concurrency control' +generated_from: + domain_map: 'skills/_artifacts/domain_map.yaml' + skill_spec: 'skills/_artifacts/skill_spec.md' +generated_at: '2026-03-15' + +skills: + - name: 'Hono Integration' + slug: 'hono-integration' + type: 'framework' + domain: 'framework-integration' + path: 'skills/hono-integration/SKILL.md' + description: > + Set up t7mMiddleware in a Hono app. Covers c.transform(), c.transformMany(), + extras object (includes, wrapper, debug, props), query-based includes + (?include=), status codes, and response headers. Use when wiring t7m into + Hono routes. + requires: + - 'build-transformer' + sources: + - 'tkoehlerlg/t7m:src/hono/middleware.ts' + - 'tkoehlerlg/t7m:src/hono/augment.ts' + - 'tkoehlerlg/t7m:src/hono/types.ts' + + - name: 'Elysia Integration' + slug: 'elysia-integration' + type: 'framework' + domain: 'framework-integration' + path: 'skills/elysia-integration/SKILL.md' + description: > + Set up t7mPlugin in an Elysia app. Covers transform(), transformMany(), + extras object, query-based includes, plain data return (not Response). + Key difference from Hono: no status codes in transform, extras optional + when no props. Use when wiring t7m into Elysia routes. + requires: + - 'build-transformer' + sources: + - 'tkoehlerlg/t7m:src/elysia/plugin.ts' + - 'tkoehlerlg/t7m:src/elysia/types.ts' + + - name: 'Build Transformer' + slug: 'build-transformer' + type: 'core' + domain: 'transformer-authoring' + path: 'skills/build-transformer/SKILL.md' + description: > + Create t7m transformers with AbstractTransformer. Covers data() method, + includesMap (optional output properties only), props (conditionally required), + Cache with .call(), nested transformers record, clearCacheOnTransform, + concurrency and includesConcurrency. Use when building or modifying + any transformer class. + sources: + - 'tkoehlerlg/t7m:src/abstractTransformer.ts' + - 'tkoehlerlg/t7m:src/lib/cache.ts' + - 'tkoehlerlg/t7m:src/lib/semaphore.ts' + - 'tkoehlerlg/t7m:src/types.ts' + + - name: 'Production Readiness' + slug: 'production-readiness' + type: 'lifecycle' + domain: 'production-ops' + path: 'skills/production-readiness/SKILL.md' + description: > + Configure t7m for production. Covers concurrency limits (constructor option + for transformMany), includesConcurrency (class property for per-include + throttling), Cache maxSize, clearCacheOnTransform tuning, Cloudflare Workers + subrequest limits. Use before deploying t7m to production or Cloudflare. + requires: + - 'build-transformer' + sources: + - 'tkoehlerlg/t7m:src/abstractTransformer.ts' + - 'tkoehlerlg/t7m:src/lib/cache.ts' + - 'tkoehlerlg/t7m:src/lib/semaphore.ts' diff --git a/skills/build-transformer/SKILL.md b/skills/build-transformer/SKILL.md new file mode 100644 index 0000000..170ff11 --- /dev/null +++ b/skills/build-transformer/SKILL.md @@ -0,0 +1,266 @@ +--- +name: build-transformer +description: > + Create t7m transformers with AbstractTransformer. Covers data() method, + includesMap (optional output properties only), props (conditionally required), + Cache with .call(), nested transformers record, clearCacheOnTransform, + concurrency and includesConcurrency. Use when building or modifying any + transformer class. +type: core +library: t7m +library_version: '1.0.0' +sources: + - 'tkoehlerlg/t7m:src/abstractTransformer.ts' + - 'tkoehlerlg/t7m:src/lib/cache.ts' + - 'tkoehlerlg/t7m:src/lib/semaphore.ts' + - 'tkoehlerlg/t7m:src/types.ts' +--- + +# t7m — Build Transformer + +## Setup + +```typescript +import { AbstractTransformer } from 't7m' + +type UserInput = { id: string; firstName: string; lastName: string; avatarUrl: string | null } +type UserOutput = { id: string; fullName: string; avatar?: string } + +class UserTransformer extends AbstractTransformer { + protected data(input: UserInput) { + return { + id: input.id, + fullName: `${input.firstName} ${input.lastName}`, + } + } +} + +const userTransformer = new UserTransformer() +const result = await userTransformer.transform({ input: user }) +``` + +All transform methods take a single params object: `{ input, includes?, props? }`. + +## Core Patterns + +### Add optional includes + +Includes resolve optional output properties on demand. Only properties marked `?` in the output type can be includes. + +```typescript +type PostOutput = { title: string; author?: AuthorOutput } + +class PostTransformer extends AbstractTransformer { + protected data(input: PostInput) { + return { title: input.title } + } + + protected readonly includesMap = { + author: async (input: PostInput) => { + const author = await db.users.findUnique({ where: { id: input.authorId } }) + return authorTransformer.transform({ input: author }) + }, + } +} + +// Request includes when transforming +const post = await postTransformer.transform({ input: post, includes: ['author'] }) +``` + +### Add required props + +When a transformer needs external dependencies, define a Props type. Props become required in transform calls. + +```typescript +type Props = { db: Database; currentUserId: string } + +class UserTransformer extends AbstractTransformer { + protected data(input: UserInput, props: Props) { + return { + id: input.id, + fullName: input.name, + isCurrentUser: input.id === props.currentUserId, + } + } +} + +// Props is now REQUIRED +await userTransformer.transform({ input: user, props: { db, currentUserId: '123' } }) +``` + +### Set up caching + +Cache uses `.call()` to invoke cached functions. Caches auto-clear between transform batches by default. + +```typescript +import { AbstractTransformer, Cache } from 't7m' + +class PostTransformer extends AbstractTransformer { + cache = { + authorProfile: new Cache((userId: string) => db.users.findUnique({ where: { id: userId } })), + } + + protected async data(input: PostInput) { + const author = await this.cache.authorProfile.call(input.authorId) + return { title: input.title, authorName: author.name } + } +} +``` + +Cache options: `{ on?: (keyof arg)[], maxSize?: number }`. Use `on` to specify cache key fields for object arguments. + +### Nest transformers + +Use the `transformers` record (not an array) to compose transformers. Nested transformer caches propagate clearing. + +```typescript +class PostTransformer extends AbstractTransformer { + transformers = { + author: new AuthorTransformer(), + } + + protected readonly includesMap = { + author: async (input: PostInput) => { + const author = await getAuthor(input.authorId) + return this.transformers.author.transform({ input: author }) + }, + } + + protected data(input: PostInput) { + return { title: input.title } + } +} +``` + +You can also wrap nested transformers in Cache for lazy instantiation: `transformers = { author: new Cache(() => new AuthorTransformer()) }`. + +## Common Mistakes + +### CRITICAL Passing positional arguments to transform + +Wrong: + +```typescript +await transformer.transform(user) +await transformer.transform(user, { includes: ['posts'] }) +``` + +Correct: + +```typescript +await transformer.transform({ input: user }) +await transformer.transform({ input: user, includes: ['posts'] }) +``` + +All transform methods take a single params object — not positional arguments. + +Source: src/abstractTransformer.ts + +### CRITICAL Using cache.get() instead of cache.call() + +Wrong: + +```typescript +const user = await this.cache.userProfile.get(input.userId) +``` + +Correct: + +```typescript +const user = await this.cache.userProfile.call(input.userId) +``` + +Cache exposes `.call()` to invoke the cached function. `.get()` does not exist. + +Source: src/lib/cache.ts + +### CRITICAL Putting required properties in includesMap + +Wrong: + +```typescript +type Output = { name: string; avatar: string } +// avatar is required — cannot be an include +includesMap = { avatar: async () => '...' } +``` + +Correct: + +```typescript +type Output = { name: string; avatar?: string } +// avatar is optional — can be an include +includesMap = { avatar: async () => '...' } +``` + +Includes only work with optional output properties. This is enforced by `OnlyPossiblyUndefined` at the type level. + +Source: src/lib/types.ts + +### HIGH Passing clearCacheOnTransform to Cache constructor + +Wrong: + +```typescript +new Cache(fn, { clearCacheOnTransform: false }) +``` + +Correct: + +```typescript +class MyTransformer extends AbstractTransformer { + constructor() { + super({ clearCacheOnTransform: false }) + } +} +``` + +`clearCacheOnTransform` is an AbstractTransformer constructor option. Cache options are `{ on?, maxSize? }`. + +Source: src/abstractTransformer.ts + +### HIGH Using transformers as an array + +Wrong: + +```typescript +transformers = [this.authorTransformer] +``` + +Correct: + +```typescript +transformers = { author: this.authorTransformer } +``` + +`transformers` is a `Record AnyAbstractTransformer>>`, not an array. + +Source: src/abstractTransformer.ts + +### HIGH Setting includesConcurrency in constructor + +Wrong: + +```typescript +constructor() { + super({ includesConcurrency: { posts: 3 } }) +} +``` + +Correct: + +```typescript +includesConcurrency = { posts: 3 } +``` + +`includesConcurrency` is a class property (like `includesMap`), not a constructor parameter. + +Source: src/abstractTransformer.ts + +### HIGH Tension: simplicity vs production safety + +Default transformer config (no concurrency limits, `clearCacheOnTransform: true`) works for development but causes issues under load or on Cloudflare. Agents optimizing for clean code tend to skip production config because the defaults "just work" in dev. + +See also: production-readiness/SKILL.md § Common Mistakes + +See also: hono-integration/SKILL.md — for using transformers in Hono routes +See also: elysia-integration/SKILL.md — for using transformers in Elysia routes diff --git a/skills/elysia-integration/SKILL.md b/skills/elysia-integration/SKILL.md new file mode 100644 index 0000000..03afb91 --- /dev/null +++ b/skills/elysia-integration/SKILL.md @@ -0,0 +1,167 @@ +--- +name: elysia-integration +description: > + Set up t7mPlugin in an Elysia app. Covers transform(), transformMany(), + extras object (includes, wrapper, debug, props), query-based includes + (?include=), plain data return (not Response). Key difference from Hono: + no status codes in transform, extras optional when no props. Use when + wiring t7m into Elysia routes. +type: framework +library: t7m +library_version: '1.0.0' +requires: + - build-transformer +sources: + - 'tkoehlerlg/t7m:src/elysia/plugin.ts' + - 'tkoehlerlg/t7m:src/elysia/types.ts' +--- + +This skill builds on build-transformer. Read it first for how to create transformers. + +# t7m — Elysia Integration + +## Setup + +```typescript +import { Elysia } from 'elysia' +import { t7mPlugin } from 't7m/elysia' + +const app = new Elysia() + .use(t7mPlugin()) + .get('/users/:id', async ({ transform, params }) => { + const user = await getUser(params.id) + return transform(user, userTransformer) + }) +``` + +## Core Patterns + +### Return transformed single object + +```typescript +app.get('/users/:id', async ({ transform, params }) => { + const user = await db.users.findUnique({ where: { id: params.id } }) + return transform(user, userTransformer) +}) +``` + +### Return transformed array + +```typescript +app.get('/users', async ({ transformMany }) => { + const users = await db.users.findMany() + return transformMany(users, userTransformer) +}) +``` + +### Pass props when transformer requires them + +```typescript +app.get('/users/:id', async ({ transform, params, store }) => { + const user = await getUser(params.id) + return transform(user, userTransformer, { + props: { db, currentUserId: store.userId }, + }) +}) +``` + +### Query-based includes with wrapper + +Clients request includes via `?include=posts,comments`. The plugin parses this automatically. + +```typescript +app.get('/users/:id', async ({ transform, params }) => { + const user = await getUser(params.id) + return transform(user, userTransformer, { + wrapper: (data) => ({ success: true, data }), + debug: true, + }) +}) +``` + +When the transformer has no Props type, extras is optional — you can call `transform(user, transformer)` without the third argument. + +## Common Mistakes + +### HIGH Expecting a Response object like Hono + +Wrong: + +```typescript +app.get('/users/:id', async ({ transform, params }) => { + const user = await getUser(params.id) + const response = await transform(user, transformer) + return new Response(JSON.stringify(response)) +}) +``` + +Correct: + +```typescript +app.get('/users/:id', async ({ transform, params }) => { + const user = await getUser(params.id) + return transform(user, transformer) +}) +``` + +Elysia's transform() returns plain data — Elysia handles serialization. Wrapping in Response double-serializes. + +Source: src/elysia/plugin.ts + +### HIGH Trying to set status codes in transform call + +Wrong: + +```typescript +return transform(user, transformer, { status: 201 }) +``` + +Correct: + +```typescript +set.status = 201 +return transform(user, transformer) +``` + +Unlike Hono, Elysia's transform has no status parameter. Use Elysia's `set.status` instead. + +Source: src/elysia/plugin.ts + +### MEDIUM Passing empty extras when no props needed + +Wrong: + +```typescript +return transform(user, transformer, {}) +``` + +Correct: + +```typescript +return transform(user, transformer) +``` + +When the transformer has no Props type, extras is optional. Passing an empty object is unnecessary noise. + +Source: src/elysia/types.ts + +### HIGH Passing props directly instead of in extras + +Wrong: + +```typescript +return transform(user, transformer, { db }) +``` + +Correct: + +```typescript +return transform(user, transformer, { props: { db } }) +``` + +The third argument is the extras object containing `props`, `includes`, `wrapper`, and `debug` — not the props value directly. + +Source: src/elysia/types.ts + +See also: hono-integration/SKILL.md — if switching frameworks, Hono returns typed Response and supports status codes in transform +See also: production-readiness/SKILL.md — configure concurrency and caching before deployment diff --git a/skills/hono-integration/SKILL.md b/skills/hono-integration/SKILL.md new file mode 100644 index 0000000..2862652 --- /dev/null +++ b/skills/hono-integration/SKILL.md @@ -0,0 +1,171 @@ +--- +name: hono-integration +description: > + Set up t7mMiddleware in a Hono app. Covers c.transform(), c.transformMany(), + extras object (includes, wrapper, debug, props), query-based includes + (?include=), status codes, and response headers. Use when wiring t7m into + Hono routes or returning transformed API responses from Hono handlers. +type: framework +library: t7m +library_version: '1.0.0' +requires: + - build-transformer +sources: + - 'tkoehlerlg/t7m:src/hono/middleware.ts' + - 'tkoehlerlg/t7m:src/hono/augment.ts' + - 'tkoehlerlg/t7m:src/hono/types.ts' +--- + +This skill builds on build-transformer. Read it first for how to create transformers. + +# t7m — Hono Integration + +## Setup + +```typescript +import { Hono } from 'hono' +import { t7mMiddleware } from 't7m/hono' + +const app = new Hono() + +// Register middleware BEFORE routes +app.use(t7mMiddleware) + +app.get('/users/:id', async (c) => { + const user = await getUser(c.req.param('id')) + return c.transform(user, userTransformer) +}) +``` + +## Core Patterns + +### Return transformed single object + +```typescript +app.get('/users/:id', async (c) => { + const user = await db.users.findUnique({ where: { id: c.req.param('id') } }) + return c.transform(user, userTransformer) +}) +``` + +### Return transformed array + +```typescript +app.get('/users', async (c) => { + const users = await db.users.findMany() + return c.transformMany(users, userTransformer) +}) +``` + +### Pass props when transformer requires them + +```typescript +app.get('/users/:id', async (c) => { + const user = await getUser(c.req.param('id')) + return c.transform(user, userTransformer, { + props: { db, currentUserId: c.get('userId') }, + }) +}) +``` + +### Query-based includes with wrapper and status code + +Clients request includes via `?include=posts,comments`. The middleware parses this automatically. + +```typescript +app.post('/users', async (c) => { + const body = await c.req.json() + const user = await createUser(body) + return c.transform(user, userTransformer, { + wrapper: (data) => ({ success: true, data }), + debug: true, + }, 201, { 'X-Created-By': 'api' }) +}) +``` + +The full signature is `c.transform(input, transformer, extras?, status?, headers?)`. + +## Common Mistakes + +### CRITICAL Calling transformer directly instead of c.transform() + +Wrong: + +```typescript +app.get('/users/:id', async (c) => { + const user = await getUser(c.req.param('id')) + return c.json(await userTransformer.transform({ input: user })) +}) +``` + +Correct: + +```typescript +app.get('/users/:id', async (c) => { + const user = await getUser(c.req.param('id')) + return c.transform(user, userTransformer) +}) +``` + +Calling the transformer directly bypasses query param parsing for includes and returns untyped JSON instead of a typed response. + +Source: src/hono/middleware.ts + +### CRITICAL Not registering middleware before routes + +Wrong: + +```typescript +app.get('/users', async (c) => c.transform(users, transformer)) +app.use(t7mMiddleware) +``` + +Correct: + +```typescript +app.use(t7mMiddleware) +app.get('/users', async (c) => c.transform(users, transformer)) +``` + +Hono applies middleware in registration order — routes registered before t7mMiddleware won't have c.transform() available. + +Source: src/hono/middleware.ts + +### HIGH Wrapping c.transform() result in c.json() + +Wrong: + +```typescript +return c.json(await c.transform(user, transformer)) +``` + +Correct: + +```typescript +return c.transform(user, transformer) +``` + +c.transform() already returns a typed JSON response — wrapping in c.json() double-serializes the output. + +Source: src/hono/middleware.ts + +### HIGH Passing props as direct argument instead of in extras + +Wrong: + +```typescript +return c.transform(user, transformer, { db }) +``` + +Correct: + +```typescript +return c.transform(user, transformer, { props: { db } }) +``` + +The third argument is the extras object containing `props`, `includes`, `wrapper`, and `debug` — not the props value directly. + +Source: src/hono/types.ts + +See also: elysia-integration/SKILL.md — if switching frameworks, Elysia returns plain data instead of Response +See also: production-readiness/SKILL.md — configure concurrency and caching before deployment diff --git a/skills/production-readiness/SKILL.md b/skills/production-readiness/SKILL.md new file mode 100644 index 0000000..f52dd13 --- /dev/null +++ b/skills/production-readiness/SKILL.md @@ -0,0 +1,215 @@ +--- +name: production-readiness +description: > + Configure t7m for production. Covers concurrency limits (constructor option + for transformMany), includesConcurrency (class property for per-include + throttling), Cache maxSize, clearCacheOnTransform tuning, Cloudflare Workers + subrequest limits. Use before deploying t7m to production or Cloudflare. +type: lifecycle +library: t7m +library_version: '1.0.0' +requires: + - build-transformer +sources: + - 'tkoehlerlg/t7m:src/abstractTransformer.ts' + - 'tkoehlerlg/t7m:src/lib/cache.ts' + - 'tkoehlerlg/t7m:src/lib/semaphore.ts' +--- + +This skill builds on build-transformer. Read it first for how to create transformers. + +# t7m — Production Readiness + +## Setup + +A production-ready transformer with concurrency limits, per-include throttling, and bounded caches: + +```typescript +import { AbstractTransformer, Cache } from 't7m' + +class UserTransformer extends AbstractTransformer { + constructor() { + super({ + concurrency: 10, + clearCacheOnTransform: true, + }) + } + + includesConcurrency = { + posts: 3, + followers: 5, + } + + cache = { + profile: new Cache((id: string) => fetchProfile(id), { maxSize: 200 }), + } + + protected data(input: UserInput, props: Props) { + return { id: input.id, name: input.name } + } + + protected readonly includesMap = { + posts: async (input: UserInput, props: Props) => { + return fetchPosts(input.id) + }, + followers: async (input: UserInput, props: Props) => { + return fetchFollowers(input.id) + }, + } +} +``` + +## Core Patterns + +### Concurrency limits for batch transforms + +`concurrency` in the constructor limits parallel items in `transformMany` / `_transformMany`. It does NOT limit single `transform()` calls. + +```typescript +class UserTransformer extends AbstractTransformer { + constructor() { + // Process max 10 users in parallel during transformMany + super({ concurrency: 10 }) + } + + protected data(input: UserInput) { + return { id: input.id, name: input.name } + } +} + +// This respects concurrency: 10 +await transformer.transformMany({ inputs: users }) + +// This does NOT — each call runs independently +await Promise.all(users.map(u => transformer.transform({ input: u }))) +``` + +### Per-include concurrency + +`includesConcurrency` is a class property that limits concurrent executions of specific include functions. Applies to ALL transform methods (both single and batch). + +```typescript +class PostTransformer extends AbstractTransformer { + // Limit concurrent API calls per include type + includesConcurrency = { + author: 3, + comments: 5, + } + + protected readonly includesMap = { + author: async (input: PostInput) => fetchAuthor(input.authorId), + comments: async (input: PostInput) => fetchComments(input.id), + } + + protected data(input: PostInput) { + return { title: input.title } + } +} +``` + +### Bounded caches + +Set `maxSize` to prevent unbounded cache growth. Oldest entries are evicted when the limit is exceeded. + +```typescript +cache = { + userProfile: new Cache( + (userId: string) => db.users.findUnique({ where: { id: userId } }), + { maxSize: 500 }, + ), + expensiveComputation: new Cache( + (input: { type: string; region: string }) => compute(input), + { on: ['type', 'region'], maxSize: 100 }, + ), +} +``` + +### Cloudflare Workers deployment + +Cloudflare Workers have a 50 subrequest limit per invocation. Set `concurrency` low enough that your batch transforms stay within this budget. + +```typescript +class ApiTransformer extends AbstractTransformer { + constructor() { + // Cloudflare: 50 subrequests max, leave headroom for other calls + super({ concurrency: 5 }) + } + + includesConcurrency = { + relatedItems: 2, + } +} +``` + +## Common Mistakes + +### CRITICAL No concurrency limit on Cloudflare Workers + +Wrong: + +```typescript +class UserTransformer extends AbstractTransformer { + constructor() { + super() + } +} +// transformMany with 100 users = 100 parallel subrequests = Cloudflare error +``` + +Correct: + +```typescript +class UserTransformer extends AbstractTransformer { + constructor() { + super({ concurrency: 5 }) + } +} +``` + +Cloudflare Workers have a 50 subrequest limit per invocation. Without concurrency limits, transformMany fires all items in parallel and exceeds this limit. + +Source: maintainer interview + +### HIGH Using Promise.all instead of transformMany for batches + +Wrong: + +```typescript +// concurrency: 5 is set but has no effect here +await Promise.all(users.map(u => transformer.transform({ input: u }))) +``` + +Correct: + +```typescript +// concurrency: 5 limits parallel processing +await transformer.transformMany({ inputs: users }) +``` + +`concurrency` only throttles `transformMany` / `_transformMany`. Single `transform()` calls run independently and bypass the concurrency limit. + +Source: src/abstractTransformer.ts + +### HIGH Unbounded cache without maxSize on high-cardinality keys + +Wrong: + +```typescript +cache = { + userProfile: new Cache((id: string) => fetchUser(id)), +} +``` + +Correct: + +```typescript +cache = { + userProfile: new Cache((id: string) => fetchUser(id), { maxSize: 500 }), +} +``` + +Without `maxSize`, the cache grows without limit. High-cardinality keys (user IDs, session tokens) cause memory issues in long-running processes. + +Source: src/lib/cache.ts + +See also: build-transformer/SKILL.md — for transformer creation patterns From 3d44ef6695871cc006d62199e7ec13e7ad3bf60a Mon Sep 17 00:00:00 2001 From: tkoehlerlg Date: Sun, 15 Mar 2026 13:43:41 +0100 Subject: [PATCH 2/4] docs: add @tanstack/intent usage to README Co-Authored-By: Claude Opus 4.6 --- README.md | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 8962b89..4ac3f84 100644 --- a/README.md +++ b/README.md @@ -10,17 +10,31 @@ Works with Hono and Elysia. No overhead — 1,000 objects with includes in under *t7m = t(ransfor)m - 7 letters between t and m* -## AI Agent Skill +## AI Agent Skills -t7m ships with a [Claude Code skill](https://code.claude.com/docs/en/skills) that teaches AI coding agents how to build transformers correctly — includes, cache, props, nested transformers, and framework integration. +t7m ships with skills that teach AI coding agents how to build transformers correctly — includes, cache, props, nested transformers, and framework integration. -Install it with the [Vercel Skills CLI](https://github.com/vercel-labs/skills): +### Using [@tanstack/intent](https://github.com/tanstack/intent) + +If you use an AI coding agent, discover and install t7m's skills automatically: + +```bash +# List available skills from installed packages +npx @tanstack/intent@latest list + +# Generate skill mappings for your agent config (AGENTS.md, CLAUDE.md, .cursorrules, etc.) +npx @tanstack/intent@latest install +``` + +Skills are versioned with t7m — the guidance always matches the library version you have installed. + +### Using [Vercel Skills CLI](https://github.com/vercel-labs/skills) ```bash npx skills add tkoehlerlg/t7m ``` -This works with Claude Code, Cursor, Codex, and other agents that support the open agent skills standard. +Both methods work with Claude Code, Cursor, Codex, and other agents that support agent skills. ## Quick Start From 1bb772a882e3781cc849185815b2c973f88e2afc Mon Sep 17 00:00:00 2001 From: tkoehlerlg Date: Sun, 15 Mar 2026 13:49:18 +0100 Subject: [PATCH 3/4] fix: correct Hono extras requirement, add missing failure modes, restore Claude Code skill info Co-Authored-By: Claude Opus 4.6 --- README.md | 6 ++++- skills/_artifacts/domain_map.yaml | 22 ++++++++++++++++- skills/_artifacts/skill_spec.md | 18 +++++++------- skills/hono-integration/SKILL.md | 36 +++++++++++++++++++++------- skills/production-readiness/SKILL.md | 6 ++--- 5 files changed, 67 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 4ac3f84..1dffbde 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,11 @@ Skills are versioned with t7m — the guidance always matches the library versio npx skills add tkoehlerlg/t7m ``` -Both methods work with Claude Code, Cursor, Codex, and other agents that support agent skills. +### Claude Code Skill + +t7m also ships with a [Claude Code skill](https://code.claude.com/docs/en/skills) in `.claude/skills/build-transformer/` — it's loaded automatically when Claude Code detects t7m usage in your project. + +All methods work with Claude Code, Cursor, Codex, and other agents that support agent skills. ## Quick Start diff --git a/skills/_artifacts/domain_map.yaml b/skills/_artifacts/domain_map.yaml index 7b820cb..bbe8fe6 100644 --- a/skills/_artifacts/domain_map.yaml +++ b/skills/_artifacts/domain_map.yaml @@ -67,7 +67,17 @@ skills: return c.transform(user, userTransformer, { db }) correct_pattern: | return c.transform(user, userTransformer, { props: { db } }) - source: 'src/hono/middleware.ts' + source: 'src/hono/types.ts' + priority: 'HIGH' + status: 'active' + + - mistake: 'Omitting the required extras argument' + mechanism: 'Unlike Elysia, Hono extras is always required — pass {} when no options needed' + wrong_pattern: | + return c.transform(user, transformer) + correct_pattern: | + return c.transform(user, transformer, {}) + source: 'src/hono/types.ts' priority: 'HIGH' status: 'active' @@ -137,6 +147,16 @@ skills: priority: 'MEDIUM' status: 'active' + - mistake: 'Passing props directly instead of in extras' + mechanism: 'The third argument is the extras object containing props, includes, wrapper, and debug — not the props value directly' + wrong_pattern: | + return transform(user, transformer, { db }) + correct_pattern: | + return transform(user, transformer, { props: { db } }) + source: 'src/elysia/types.ts' + priority: 'HIGH' + status: 'active' + - mistake: 'Trying to set status codes in transform return' mechanism: 'Unlike Hono, Elysia transform has no status parameter — use Elysia set.status instead' wrong_pattern: | diff --git a/skills/_artifacts/skill_spec.md b/skills/_artifacts/skill_spec.md index 4efb38c..c0951cd 100644 --- a/skills/_artifacts/skill_spec.md +++ b/skills/_artifacts/skill_spec.md @@ -14,29 +14,31 @@ t7m is a TypeScript library for transforming API output with type-safe includes, | Skill | Type | Domain | What it covers | Failure modes | | --- | --- | --- | --- | --- | -| hono-integration | framework | framework-integration | t7mMiddleware, c.transform(), extras, query params, status codes | 4 | -| elysia-integration | framework | framework-integration | t7mPlugin, transform(), extras, query params, plain data return | 3 | +| hono-integration | framework | framework-integration | t7mMiddleware, c.transform(), extras (required), query params, status codes | 5 | +| elysia-integration | framework | framework-integration | t7mPlugin, transform(), extras (optional when no props), query params, plain data return | 4 | | build-transformer | core | transformer-authoring | AbstractTransformer, data(), includes, props, cache, nested transformers, concurrency | 6 | | production-readiness | lifecycle | production-ops | concurrency limits, cache maxSize, clearCacheOnTransform, Cloudflare constraints | 3 | ## Failure Mode Inventory -### Hono Integration (4 failure modes) +### Hono Integration (5 failure modes) | # | Mistake | Priority | Source | Cross-skill? | | --- | --- | --- | --- | --- | | 1 | Calling transformer.transform() instead of c.transform() | CRITICAL | src/hono/middleware.ts | — | -| 2 | Passing props as third argument instead of in extras | HIGH | src/hono/middleware.ts | — | -| 3 | Not registering middleware before routes | CRITICAL | src/hono/middleware.ts | — | -| 4 | Returning c.transform result inside c.json() | HIGH | src/hono/middleware.ts | — | +| 2 | Passing props as third argument instead of in extras | HIGH | src/hono/types.ts | — | +| 3 | Omitting the required extras argument | HIGH | src/hono/types.ts | — | +| 4 | Not registering middleware before routes | CRITICAL | src/hono/middleware.ts | — | +| 5 | Returning c.transform result inside c.json() | HIGH | src/hono/middleware.ts | — | -### Elysia Integration (3 failure modes) +### Elysia Integration (4 failure modes) | # | Mistake | Priority | Source | Cross-skill? | | --- | --- | --- | --- | --- | | 1 | Expecting a Response object like Hono | HIGH | src/elysia/plugin.ts | — | | 2 | Passing extras when no props needed | MEDIUM | src/elysia/types.ts | — | -| 3 | Trying to set status codes in transform return | HIGH | src/elysia/plugin.ts | — | +| 3 | Passing props directly instead of in extras | HIGH | src/elysia/types.ts | — | +| 4 | Trying to set status codes in transform return | HIGH | src/elysia/plugin.ts | — | ### Build Transformer (6 failure modes) diff --git a/skills/hono-integration/SKILL.md b/skills/hono-integration/SKILL.md index 2862652..dcf581a 100644 --- a/skills/hono-integration/SKILL.md +++ b/skills/hono-integration/SKILL.md @@ -33,7 +33,7 @@ app.use(t7mMiddleware) app.get('/users/:id', async (c) => { const user = await getUser(c.req.param('id')) - return c.transform(user, userTransformer) + return c.transform(user, userTransformer, {}) }) ``` @@ -41,10 +41,12 @@ app.get('/users/:id', async (c) => { ### Return transformed single object +The `extras` object (third argument) is always required in Hono — pass `{}` when you don't need any options. This differs from Elysia, where extras is optional. + ```typescript app.get('/users/:id', async (c) => { const user = await db.users.findUnique({ where: { id: c.req.param('id') } }) - return c.transform(user, userTransformer) + return c.transform(user, userTransformer, {}) }) ``` @@ -53,7 +55,7 @@ app.get('/users/:id', async (c) => { ```typescript app.get('/users', async (c) => { const users = await db.users.findMany() - return c.transformMany(users, userTransformer) + return c.transformMany(users, userTransformer, {}) }) ``` @@ -83,7 +85,7 @@ app.post('/users', async (c) => { }) ``` -The full signature is `c.transform(input, transformer, extras?, status?, headers?)`. +The full signature is `c.transform(input, transformer, extras, status?, headers?)`. The `extras` argument is always required. ## Common Mistakes @@ -103,7 +105,7 @@ Correct: ```typescript app.get('/users/:id', async (c) => { const user = await getUser(c.req.param('id')) - return c.transform(user, userTransformer) + return c.transform(user, userTransformer, {}) }) ``` @@ -124,7 +126,7 @@ Correct: ```typescript app.use(t7mMiddleware) -app.get('/users', async (c) => c.transform(users, transformer)) +app.get('/users', async (c) => c.transform(users, transformer, {})) ``` Hono applies middleware in registration order — routes registered before t7mMiddleware won't have c.transform() available. @@ -136,13 +138,13 @@ Source: src/hono/middleware.ts Wrong: ```typescript -return c.json(await c.transform(user, transformer)) +return c.json(await c.transform(user, transformer, {})) ``` Correct: ```typescript -return c.transform(user, transformer) +return c.transform(user, transformer, {}) ``` c.transform() already returns a typed JSON response — wrapping in c.json() double-serializes the output. @@ -167,5 +169,23 @@ The third argument is the extras object containing `props`, `includes`, `wrapper Source: src/hono/types.ts +### HIGH Omitting the required extras argument + +Wrong: + +```typescript +return c.transform(user, transformer) +``` + +Correct: + +```typescript +return c.transform(user, transformer, {}) +``` + +Unlike Elysia, the Hono `extras` argument is always required. Pass `{}` when you don't need includes, wrapper, debug, or props. + +Source: src/hono/types.ts + See also: elysia-integration/SKILL.md — if switching frameworks, Elysia returns plain data instead of Response See also: production-readiness/SKILL.md — configure concurrency and caching before deployment diff --git a/skills/production-readiness/SKILL.md b/skills/production-readiness/SKILL.md index f52dd13..12820f8 100644 --- a/skills/production-readiness/SKILL.md +++ b/skills/production-readiness/SKILL.md @@ -126,12 +126,12 @@ cache = { ### Cloudflare Workers deployment -Cloudflare Workers have a 50 subrequest limit per invocation. Set `concurrency` low enough that your batch transforms stay within this budget. +Cloudflare Workers have strict subrequest limits (6 concurrent, 50 total per invocation). Set `concurrency` low enough that your batch transforms stay within this budget. ```typescript class ApiTransformer extends AbstractTransformer { constructor() { - // Cloudflare: 50 subrequests max, leave headroom for other calls + // Cloudflare: 6 concurrent / 50 total subrequests, keep concurrency low super({ concurrency: 5 }) } @@ -166,7 +166,7 @@ class UserTransformer extends AbstractTransformer { } ``` -Cloudflare Workers have a 50 subrequest limit per invocation. Without concurrency limits, transformMany fires all items in parallel and exceeds this limit. +Cloudflare Workers have strict subrequest limits (6 concurrent, 50 total per invocation). Without concurrency limits, transformMany fires all items in parallel and exceeds this limit. Source: maintainer interview From 328cface16ceee1869e77870b5afb1ce0203674c Mon Sep 17 00:00:00 2001 From: tkoehlerlg Date: Sun, 15 Mar 2026 15:13:33 +0100 Subject: [PATCH 4/4] fix: align domain_map.yaml with corrected Hono extras and Cloudflare limits Co-Authored-By: Claude Opus 4.6 --- skills/_artifacts/domain_map.yaml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/skills/_artifacts/domain_map.yaml b/skills/_artifacts/domain_map.yaml index bbe8fe6..5af77b0 100644 --- a/skills/_artifacts/domain_map.yaml +++ b/skills/_artifacts/domain_map.yaml @@ -55,7 +55,7 @@ skills: correct_pattern: | app.get('/users/:id', async (c) => { const user = await getUser(c.req.param('id')) - return c.transform(user, userTransformer) + return c.transform(user, userTransformer, {}) }) source: 'src/hono/middleware.ts' priority: 'CRITICAL' @@ -88,7 +88,7 @@ skills: app.use(t7mMiddleware) correct_pattern: | app.use(t7mMiddleware) - app.get('/users', async (c) => c.transform(user, transformer)) + app.get('/users', async (c) => c.transform(user, transformer, {})) source: 'src/hono/middleware.ts' priority: 'CRITICAL' status: 'active' @@ -96,9 +96,9 @@ skills: - mistake: 'Returning c.transform result inside c.json()' mechanism: 'c.transform() already returns a typed JSON response — wrapping it in c.json() double-serializes' wrong_pattern: | - return c.json(await c.transform(user, transformer)) + return c.json(await c.transform(user, transformer, {})) correct_pattern: | - return c.transform(user, transformer) + return c.transform(user, transformer, {}) source: 'src/hono/middleware.ts' priority: 'HIGH' status: 'active' @@ -275,14 +275,14 @@ skills: - 'Deploy t7m to Cloudflare Workers within subrequest limits' failure_modes: - mistake: 'No concurrency limit on Cloudflare Workers' - mechanism: 'Cloudflare Workers have a 50 subrequest limit per invocation — unbounded transformMany with external calls will hit this' + mechanism: 'Cloudflare Workers have strict subrequest limits (6 concurrent, 50 total) — unbounded transformMany with external calls will hit this' wrong_pattern: | class UserTransformer extends AbstractTransformer { constructor() { super() } } correct_pattern: | class UserTransformer extends AbstractTransformer { - constructor() { super({ concurrency: 10 }) } + constructor() { super({ concurrency: 5 }) } } source: 'maintainer interview' priority: 'CRITICAL'