ts-procedures 6.0.0 → 6.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/agent_config/claude-code/agents/ts-procedures-architect.md +1 -1
  2. package/agent_config/claude-code/skills/ts-procedures/SKILL.md +1 -1
  3. package/agent_config/claude-code/skills/ts-procedures/anti-patterns.md +2 -2
  4. package/agent_config/claude-code/skills/ts-procedures/api-reference.md +15 -27
  5. package/agent_config/claude-code/skills/ts-procedures/patterns.md +11 -4
  6. package/agent_config/claude-code/skills/ts-procedures-review/checklist.md +2 -2
  7. package/agent_config/copilot/copilot-instructions.md +3 -2
  8. package/agent_config/cursor/cursorrules +3 -2
  9. package/build/codegen/targets/kotlin/ajsc-adapter.d.ts +24 -0
  10. package/build/codegen/targets/kotlin/ajsc-adapter.js +33 -0
  11. package/build/codegen/targets/kotlin/ajsc-adapter.js.map +1 -0
  12. package/build/codegen/targets/kotlin/ajsc-adapter.test.d.ts +1 -0
  13. package/build/codegen/targets/kotlin/ajsc-adapter.test.js +19 -0
  14. package/build/codegen/targets/kotlin/ajsc-adapter.test.js.map +1 -0
  15. package/build/codegen/targets/kotlin/e2e-compile.test.d.ts +1 -0
  16. package/build/codegen/targets/kotlin/e2e-compile.test.js +43 -0
  17. package/build/codegen/targets/kotlin/e2e-compile.test.js.map +1 -0
  18. package/build/codegen/targets/kotlin/emit-route-kotlin.d.ts +11 -0
  19. package/build/codegen/targets/kotlin/emit-route-kotlin.js +73 -0
  20. package/build/codegen/targets/kotlin/emit-route-kotlin.js.map +1 -0
  21. package/build/codegen/targets/kotlin/emit-route-kotlin.test.d.ts +1 -0
  22. package/build/codegen/targets/kotlin/emit-route-kotlin.test.js +88 -0
  23. package/build/codegen/targets/kotlin/emit-route-kotlin.test.js.map +1 -0
  24. package/build/codegen/targets/kotlin/emit-scope-kotlin.d.ts +11 -0
  25. package/build/codegen/targets/kotlin/emit-scope-kotlin.js +35 -0
  26. package/build/codegen/targets/kotlin/emit-scope-kotlin.js.map +1 -0
  27. package/build/codegen/targets/kotlin/emit-scope-kotlin.test.d.ts +1 -0
  28. package/build/codegen/targets/kotlin/emit-scope-kotlin.test.js +52 -0
  29. package/build/codegen/targets/kotlin/emit-scope-kotlin.test.js.map +1 -0
  30. package/build/codegen/targets/kotlin/format-kotlin.d.ts +4 -0
  31. package/build/codegen/targets/kotlin/format-kotlin.js +20 -0
  32. package/build/codegen/targets/kotlin/format-kotlin.js.map +1 -0
  33. package/build/codegen/targets/kotlin/format-kotlin.test.d.ts +1 -0
  34. package/build/codegen/targets/kotlin/format-kotlin.test.js +24 -0
  35. package/build/codegen/targets/kotlin/format-kotlin.test.js.map +1 -0
  36. package/build/codegen/targets/kotlin/integration.test.d.ts +1 -0
  37. package/build/codegen/targets/kotlin/integration.test.js +34 -0
  38. package/build/codegen/targets/kotlin/integration.test.js.map +1 -0
  39. package/build/implementations/http/doc-registry.d.ts +14 -19
  40. package/build/implementations/http/doc-registry.js +41 -46
  41. package/build/implementations/http/doc-registry.js.map +1 -1
  42. package/build/implementations/http/doc-registry.test.js +141 -10
  43. package/build/implementations/http/doc-registry.test.js.map +1 -1
  44. package/build/implementations/http/error-taxonomy.d.ts +11 -2
  45. package/build/implementations/http/error-taxonomy.js +24 -2
  46. package/build/implementations/http/error-taxonomy.js.map +1 -1
  47. package/build/implementations/http/route-errors.test.js +5 -6
  48. package/build/implementations/http/route-errors.test.js.map +1 -1
  49. package/build/implementations/types.d.ts +13 -1
  50. package/docs/http-integrations.md +39 -5
  51. package/docs/superpowers/plans/2026-04-24-doc-registry-simplification.md +886 -0
  52. package/docs/superpowers/plans/2026-04-24-kotlin-codegen-target.md +1265 -0
  53. package/docs/superpowers/specs/2026-04-24-kotlin-swift-codegen-design.md +401 -0
  54. package/package.json +1 -1
  55. package/src/implementations/http/README.md +4 -3
  56. package/src/implementations/http/doc-registry.test.ts +154 -10
  57. package/src/implementations/http/doc-registry.ts +46 -53
  58. package/src/implementations/http/error-taxonomy.ts +26 -2
  59. package/src/implementations/http/express-rpc/README.md +2 -2
  60. package/src/implementations/http/hono-rpc/README.md +2 -2
  61. package/src/implementations/http/hono-stream/README.md +15 -0
  62. package/src/implementations/http/route-errors.test.ts +5 -6
  63. package/src/implementations/types.ts +13 -1
@@ -37,7 +37,7 @@ When asked to plan an API or procedure set:
37
37
  - AJV is configured with `allErrors: true`, `coerceTypes: true`, `removeAdditional: true`.
38
38
  - `schema.params` and `schema.input` are mutually exclusive — defining both throws `ProcedureRegistrationError`.
39
39
  - Path param names in route template (`:id`) must match `schema.input.pathParams` property names.
40
- - Use `DocRegistry` to compose route docs from multiple builders — never manually wire `/docs` endpoints. Use `DocRegistry.fromTaxonomy(appErrors)` to seed envelope errors from your taxonomy + framework defaults in one call.
40
+ - Use `DocRegistry` to compose route docs from multiple builders — never manually wire `/docs` endpoints. Pass your taxonomy directly: `new DocRegistry({ errors: appErrors })` framework defaults are auto-merged and deduped. For errors outside your taxonomy (middleware, infrastructure, doc-only), chain `.documentError(...docs)`.
41
41
  - Two first-class peer error-handling modes: **declarative taxonomy** (`defineErrorTaxonomy` + `errors` config) OR **imperative callback** (`onError`). Neither is deprecated. Pick the taxonomy for structured apps with typed client dispatch; pick `onError` for simple apps or full response control. Mixing both is allowed — the taxonomy handles what it covers, `onError` handles the tail. The anti-pattern is `instanceof` ladders inside `onError` (see anti-pattern #20 in the skill reference) — that's exactly what the taxonomy expresses declaratively.
42
42
  - Per-route `errors: ['UseCaseError', ...]` narrows typed errors on the generated client. Declare via `APIConfig<keyof typeof appErrors & string>` / `RPCConfig<keyof typeof appErrors & string>` for compile-time typo protection.
43
43
  - Generated `_errors.ts` emits real runtime classes extending `${Service}ProcedureError` — consumers catch with `instanceof ${Service}Errors.${Name}` and access `err.body`, `err.status`, `err.procedureName`, `err.scope`. Use the generated `create${Service}Client(config)` factory to wire the error registry automatically.
@@ -171,7 +171,7 @@ The npm package ships user-facing documentation with narrative explanations and
171
171
  |------|--------|
172
172
  | `docs/core.md` | Procedures factory, Create, CreateStream, schema.input, error handling |
173
173
  | `docs/streaming.md` | Streaming procedures, AbortSignal, SSE patterns |
174
- | `docs/http-integrations.md` | Express RPC, Hono RPC/Stream/API builders, **error taxonomy (canonical)**, DocRegistry + `fromTaxonomy` |
174
+ | `docs/http-integrations.md` | Express RPC, Hono RPC/Stream/API builders, **error taxonomy (canonical)**, DocRegistry (unified constructor, `.documentError()`) |
175
175
  | `docs/client-and-codegen.md` | Client code generation, `createApiClient`/`createClient`, typed error dispatch, per-route `Errors` unions, per-call options, client-level defaults, typed RequestMeta augmentation, CLI options |
176
176
  | `CHANGELOG.md` | Release notes — see `[6.0.0]` for the current peer error-handling model (taxonomy + `onError` + `onRequestError`), per-route errors, and client runtime error classes. |
177
177
 
@@ -476,13 +476,13 @@ import { DocRegistry } from 'ts-procedures/http-docs'
476
476
  const docs = new DocRegistry({
477
477
  basePath: '/api',
478
478
  headers: [{ name: 'Authorization', description: 'Bearer token' }],
479
- errors: DocRegistry.defaultErrors(),
479
+ errors: appErrors, // your ErrorTaxonomy — framework defaults auto-merged
480
480
  }).from(rpcBuilder).from(apiBuilder).from(streamBuilder)
481
481
 
482
482
  app.get('/docs', (c) => c.json(docs.toJSON()))
483
483
  ```
484
484
 
485
- **Why:** `DocRegistry` provides a typed `DocEnvelope`, includes `defaultErrors()` with JSON Schemas for all 4 procedure error types, reads docs lazily (order-independent), and supports filtering and transformation via `toJSON()` options.
485
+ **Why:** `DocRegistry` provides a typed `DocEnvelope`, auto-merges framework error defaults (`ProcedureError`, `ProcedureValidationError`, `ProcedureYieldValidationError`, `ProcedureRegistrationError`) with JSON Schemas, reads docs lazily (order-independent), and supports filtering and transformation via `toJSON()` options.
486
486
 
487
487
  ---
488
488
 
@@ -295,13 +295,7 @@ When `toResponse` is omitted, the body defaults to `{ name: key, message: err.me
295
295
 
296
296
  Pre-built taxonomy covering `ProcedureValidationError` (400), `ProcedureYieldValidationError` (500), and direct `ProcedureError` (500, unwrapped throws only — wrapped ones fall through so user taxonomy / `unknownError` sees the real error). Applied as a fallback after the user taxonomy unless `includeDefaults: false`.
297
297
 
298
- ### taxonomyToErrorDocs(taxonomy)
299
-
300
- ```typescript
301
- function taxonomyToErrorDocs(taxonomy: ErrorTaxonomy): ErrorDoc[]
302
- ```
303
-
304
- Converts a taxonomy to the `ErrorDoc[]` format consumed by `DocRegistry.errors` — single source of truth so the documented shape cannot drift from runtime behavior.
298
+ Taxonomy-to-doc conversion is handled automatically by `DocRegistry` when you pass your taxonomy to the constructor. There is no public helper.
305
299
 
306
300
  ### resolveErrorResponse(params)
307
301
 
@@ -795,30 +789,24 @@ import type { DocEnvelope, DocRegistryConfig, DocRegistryOutputOptions, HeaderDo
795
789
  ```
796
790
 
797
791
  ```typescript
792
+ import { DocRegistry } from 'ts-procedures/http-docs'
793
+ import type { DocEnvelope, DocRegistryConfig, DocRegistryOutputOptions, HeaderDoc, ErrorDoc, DocSource } from 'ts-procedures/http-docs'
794
+
795
+ interface DocRegistryConfig {
796
+ basePath?: string
797
+ headers?: HeaderDoc[]
798
+ errors?: ErrorTaxonomy | ErrorDoc[] // polymorphic — pass taxonomy or raw docs
799
+ includeDefaults?: boolean // default: true (auto-merges framework defaults)
800
+ }
801
+
798
802
  class DocRegistry {
799
- constructor(config?: {
800
- basePath?: string
801
- headers?: HeaderDoc[]
802
- errors?: ErrorDoc[]
803
- })
803
+ constructor(config?: DocRegistryConfig)
804
804
 
805
805
  from(source: DocSource<AnyHttpRouteDoc>): this
806
+ documentError(...docs: ErrorDoc[]): this
807
+ toJSON<T = DocEnvelope>(options?: DocRegistryOutputOptions<T>): T
806
808
 
807
- toJSON<T = DocEnvelope>(options?: {
808
- filter?: (route: AnyHttpRouteDoc) => boolean
809
- transform?: (envelope: DocEnvelope) => T
810
- }): T
811
-
812
- // Framework error defaults derived from defaultErrorTaxonomy — single source
813
- // of truth so runtime and documented shapes cannot drift.
814
809
  static defaultErrors(): ErrorDoc[]
815
-
816
- // Convenience: seed envelope errors from a taxonomy + framework defaults
817
- // in one call (deduped — user entries win when keys overlap).
818
- static fromTaxonomy(
819
- taxonomy: ErrorTaxonomy,
820
- config?: Omit<DocRegistryConfig, 'errors'> & { includeDefaults?: boolean }
821
- ): DocRegistry
822
810
  }
823
811
  ```
824
812
 
@@ -875,7 +863,7 @@ type AnyHttpRouteDoc = RPCHttpRouteDoc | APIHttpRouteDoc | StreamHttpRouteDoc
875
863
 
876
864
  - `from()` stores a **reference** to the builder — routes are read lazily at `toJSON()` time, so builders can be registered before or after `.build()`
877
865
  - `toJSON()` collects routes via `flatMap(source => source.docs)`, applies optional `filter`, builds envelope, then applies optional `transform`
878
- - `defaultErrors()` returns 4 `ErrorDoc` entries describing `ProcedureError` (500), `ProcedureValidationError` (400), `ProcedureYieldValidationError` (500), `ProcedureRegistrationError` (500) — each with a JSON Schema for the error response body shape
866
+ - `defaultErrors()` returns 4 `ErrorDoc` entries describing `ProcedureError` (500), `ProcedureValidationError` (400), `ProcedureYieldValidationError` (500), `ProcedureRegistrationError` (500) — each with a JSON Schema for the error response body shape. Most consumers do not need to call this directly; the `DocRegistry` constructor auto-merges these unless `includeDefaults: false` is passed.
879
867
  - `JSON.stringify(registry)` works directly (calls `toJSON()` implicitly)
880
868
 
881
869
  ---
@@ -186,7 +186,7 @@ Seed the DocEnvelope with both taxonomy errors and framework defaults in one cal
186
186
  ```typescript
187
187
  import { DocRegistry } from 'ts-procedures/http-docs'
188
188
 
189
- const envelope = DocRegistry.fromTaxonomy(appErrors, { basePath: '/api' })
189
+ const envelope = new DocRegistry({ errors: appErrors, basePath: '/api' })
190
190
  .from(apiApp)
191
191
  .from(rpcApp)
192
192
  .toJSON()
@@ -194,6 +194,13 @@ const envelope = DocRegistry.fromTaxonomy(appErrors, { basePath: '/api' })
194
194
  // envelope.routes[i].errors: per-route subset declared on config.errors
195
195
  ```
196
196
 
197
+ ```typescript
198
+ // For errors outside your taxonomy (middleware, infrastructure, doc-only):
199
+ const docsWithExtras = new DocRegistry({ errors: appErrors, basePath: '/api' })
200
+ .from(apiBuilder)
201
+ .documentError({ name: 'RateLimitExceeded', statusCode: 429, description: 'too many requests' })
202
+ ```
203
+
197
204
  ### Typed catch blocks on the client (via codegen)
198
205
 
199
206
  The codegen emits runtime error classes (not just types) and a registry object. `createApiClient` wires the registry automatically so non-2xx responses arrive as typed class instances.
@@ -805,7 +812,7 @@ import { DocRegistry } from 'ts-procedures/http-docs'
805
812
  const docs = new DocRegistry({
806
813
  basePath: '/api',
807
814
  headers: [{ name: 'Authorization', description: 'Bearer token', required: false }],
808
- errors: DocRegistry.defaultErrors(),
815
+ errors: appErrors, // your ErrorTaxonomy — framework defaults auto-merged and deduped
809
816
  })
810
817
  .from(rpcBuilder)
811
818
  .from(apiBuilder)
@@ -824,7 +831,7 @@ app.get('/docs', (c) => c.json(docs.toJSON({
824
831
  **Key points:**
825
832
  - `from()` stores a reference — register builders before or after `.build()`
826
833
  - `toJSON()` reads docs lazily, so late-registered procedures are included
827
- - `DocRegistry.defaultErrors()` provides error schemas for all 4 procedure error types
834
+ - Pass your `ErrorTaxonomy` directly to `errors` the constructor auto-merges framework defaults and dedupes; opt out with `includeDefaults: false`
828
835
  - Accepts any object satisfying `{ readonly docs: AnyHttpRouteDoc[] }` — not limited to built-in builders
829
836
 
830
837
  ---
@@ -926,7 +933,7 @@ import { DocRegistry } from 'ts-procedures/http-docs'
926
933
  const docs = new DocRegistry({
927
934
  basePath: '/api',
928
935
  headers: [{ name: 'Authorization', description: 'Bearer token' }],
929
- errors: DocRegistry.defaultErrors(),
936
+ errors: appErrors, // your ErrorTaxonomy — framework defaults auto-merged
930
937
  })
931
938
  .from(rpcBuilder)
932
939
  .from(apiBuilder)
@@ -74,7 +74,7 @@
74
74
 
75
75
  ### SUGGESTION
76
76
  - [ ] Route documentation accessed via `builder.docs` for OpenAPI generation
77
- - [ ] Uses `DocRegistry.fromTaxonomy(appErrors)` (or plain `DocRegistry` with explicit `errors`) to compose docs from multiple builders
77
+ - [ ] Uses `new DocRegistry({ errors: appErrors })` to compose docs from multiple builders
78
78
  - [ ] Lifecycle hooks used for observability (logging, metrics)
79
79
  - [ ] Per-route `errors: [...]` declared so generated clients can narrow `catch` types
80
80
 
@@ -115,7 +115,7 @@
115
115
 
116
116
  ### SUGGESTION
117
117
  - [ ] Route documentation accessed via `builder.docs` for OpenAPI generation
118
- - [ ] Uses `DocRegistry.fromTaxonomy(appErrors)` to compose docs across builders instead of manual assembly
118
+ - [ ] Uses `new DocRegistry({ errors: appErrors })` to compose docs across builders instead of manual assembly
119
119
  - [ ] Custom `queryParser` provided if complex query string formats needed
120
120
  - [ ] Lifecycle hooks used for observability (logging, metrics)
121
121
 
@@ -218,7 +218,8 @@ class UseCaseError extends Error {
218
218
  }
219
219
 
220
220
  const appErrors = defineErrorTaxonomy({
221
- // First-match wins declare subclasses before base classes
221
+ // class: entries are topologically sorted (subclasses checked first) automatically.
222
+ // Predicate (match:) entries keep declared order — put narrower predicates first.
222
223
  UseCaseError: {
223
224
  class: UseCaseError,
224
225
  statusCode: 422,
@@ -254,7 +255,7 @@ const API = Procedures<Ctx, MyAPIConfig>()
254
255
  API.Create('GetUser', { path: '/users/:id', method: 'get', errors: ['UseCaseError'], /* ... */ }, ...)
255
256
 
256
257
  // Seed envelope errors from the taxonomy + framework defaults in one call
257
- const envelope = DocRegistry.fromTaxonomy(appErrors, { basePath: '/api' }).from(apiApp).toJSON()
258
+ const envelope = new DocRegistry({ errors: appErrors, basePath: '/api' }).from(apiApp).toJSON()
258
259
  ```
259
260
 
260
261
  ### Client-side typed catch blocks (via codegen)
@@ -218,7 +218,8 @@ class UseCaseError extends Error {
218
218
  }
219
219
 
220
220
  const appErrors = defineErrorTaxonomy({
221
- // First-match wins declare subclasses before base classes
221
+ // class: entries are topologically sorted (subclasses checked first) automatically.
222
+ // Predicate (match:) entries keep declared order — put narrower predicates first.
222
223
  UseCaseError: {
223
224
  class: UseCaseError,
224
225
  statusCode: 422,
@@ -254,7 +255,7 @@ const API = Procedures<Ctx, MyAPIConfig>()
254
255
  API.Create('GetUser', { path: '/users/:id', method: 'get', errors: ['UseCaseError'], /* ... */ }, ...)
255
256
 
256
257
  // Seed envelope errors from the taxonomy + framework defaults in one call
257
- const envelope = DocRegistry.fromTaxonomy(appErrors, { basePath: '/api' }).from(apiApp).toJSON()
258
+ const envelope = new DocRegistry({ errors: appErrors, basePath: '/api' }).from(apiApp).toJSON()
258
259
  ```
259
260
 
260
261
  ### Client-side typed catch blocks (via codegen)
@@ -0,0 +1,24 @@
1
+ export interface KotlinEmitResult {
2
+ code: string;
3
+ rootTypeName: string;
4
+ extractedTypeNames: string[];
5
+ imports: string[];
6
+ }
7
+ export interface KotlinEmitOptions {
8
+ rootTypeName: string;
9
+ inlineTypes?: boolean;
10
+ serializer?: 'kotlinx';
11
+ enumStyle?: string;
12
+ depluralize?: boolean;
13
+ arrayItemNaming?: string;
14
+ uncountableWords?: string[];
15
+ }
16
+ export interface KotlinEmitter {
17
+ emit(schema: Record<string, unknown>, opts: KotlinEmitOptions): KotlinEmitResult;
18
+ }
19
+ export declare function createStubKotlinEmitter(results: Record<string, KotlinEmitResult>): KotlinEmitter;
20
+ /**
21
+ * Resolves the production Kotlin emitter from `ajsc`. Throws a clear error
22
+ * if the ajsc package does not yet expose `emitKotlin` (Phase A pending).
23
+ */
24
+ export declare function resolveProductionKotlinEmitter(): Promise<KotlinEmitter>;
@@ -0,0 +1,33 @@
1
+ export function createStubKotlinEmitter(results) {
2
+ return {
3
+ emit(_schema, opts) {
4
+ const result = results[opts.rootTypeName];
5
+ if (result == null) {
6
+ throw new Error(`[stub-kotlin-emitter] No stubbed result for rootTypeName "${opts.rootTypeName}". ` +
7
+ `Provide one in the results map.`);
8
+ }
9
+ return result;
10
+ },
11
+ };
12
+ }
13
+ /**
14
+ * Resolves the production Kotlin emitter from `ajsc`. Throws a clear error
15
+ * if the ajsc package does not yet expose `emitKotlin` (Phase A pending).
16
+ */
17
+ export async function resolveProductionKotlinEmitter() {
18
+ // TODO(ajsc-phase-a): replace dynamic import with a static import once ajsc ships emitKotlin.
19
+ const ajsc = await import('ajsc').catch(() => null);
20
+ const emitKotlin = ajsc?.emitKotlin;
21
+ if (typeof emitKotlin !== 'function') {
22
+ throw new Error('[ts-procedures-codegen] ajsc.emitKotlin is not available. ' +
23
+ 'Kotlin codegen requires ajsc Phase A. See docs/superpowers/specs/2026-04-24-kotlin-swift-codegen-design.md.');
24
+ }
25
+ return {
26
+ emit(schema, opts) {
27
+ // ajsc's return shape is normalized to KotlinEmitResult here.
28
+ const r = emitKotlin(schema, opts);
29
+ return r;
30
+ },
31
+ };
32
+ }
33
+ //# sourceMappingURL=ajsc-adapter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ajsc-adapter.js","sourceRoot":"","sources":["../../../../src/codegen/targets/kotlin/ajsc-adapter.ts"],"names":[],"mappings":"AAqBA,MAAM,UAAU,uBAAuB,CACrC,OAAyC;IAEzC,OAAO;QACL,IAAI,CAAC,OAAO,EAAE,IAAI;YAChB,MAAM,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,YAAY,CAAC,CAAA;YACzC,IAAI,MAAM,IAAI,IAAI,EAAE,CAAC;gBACnB,MAAM,IAAI,KAAK,CACb,6DAA6D,IAAI,CAAC,YAAY,KAAK;oBACjF,iCAAiC,CACpC,CAAA;YACH,CAAC;YACD,OAAO,MAAM,CAAA;QACf,CAAC;KACF,CAAA;AACH,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,8BAA8B;IAClD,8FAA8F;IAC9F,MAAM,IAAI,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,CAAA;IACnD,MAAM,UAAU,GAAI,IAAwC,EAAE,UAAU,CAAA;IACxE,IAAI,OAAO,UAAU,KAAK,UAAU,EAAE,CAAC;QACrC,MAAM,IAAI,KAAK,CACb,4DAA4D;YAC5D,6GAA6G,CAC9G,CAAA;IACH,CAAC;IACD,OAAO;QACL,IAAI,CAAC,MAAM,EAAE,IAAI;YACf,8DAA8D;YAC9D,MAAM,CAAC,GAAI,UAA2D,CAAC,MAAM,EAAE,IAAI,CAAC,CAAA;YACpF,OAAO,CAAC,CAAA;QACV,CAAC;KACF,CAAA;AACH,CAAC"}
@@ -0,0 +1,19 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { createStubKotlinEmitter } from './ajsc-adapter.js';
3
+ describe('createStubKotlinEmitter', () => {
4
+ it('returns the configured EmitResult for the matching root name', () => {
5
+ const expected = {
6
+ code: '@Serializable data class User(val id: String)',
7
+ rootTypeName: 'User',
8
+ extractedTypeNames: [],
9
+ imports: ['kotlinx.serialization.Serializable'],
10
+ };
11
+ const emitter = createStubKotlinEmitter({ User: expected });
12
+ expect(emitter.emit({ type: 'object' }, { rootTypeName: 'User' })).toEqual(expected);
13
+ });
14
+ it('throws when asked to emit a name not in the stub map', () => {
15
+ const emitter = createStubKotlinEmitter({});
16
+ expect(() => emitter.emit({}, { rootTypeName: 'Missing' })).toThrow(/Missing/);
17
+ });
18
+ });
19
+ //# sourceMappingURL=ajsc-adapter.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ajsc-adapter.test.js","sourceRoot":"","sources":["../../../../src/codegen/targets/kotlin/ajsc-adapter.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAA;AAC7C,OAAO,EAAE,uBAAuB,EAAyB,MAAM,mBAAmB,CAAA;AAElF,QAAQ,CAAC,yBAAyB,EAAE,GAAG,EAAE;IACvC,EAAE,CAAC,8DAA8D,EAAE,GAAG,EAAE;QACtE,MAAM,QAAQ,GAAqB;YACjC,IAAI,EAAE,+CAA+C;YACrD,YAAY,EAAE,MAAM;YACpB,kBAAkB,EAAE,EAAE;YACtB,OAAO,EAAE,CAAC,oCAAoC,CAAC;SAChD,CAAA;QACD,MAAM,OAAO,GAAG,uBAAuB,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC,CAAA;QAC3D,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE,EAAE,YAAY,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAA;IACtF,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,sDAAsD,EAAE,GAAG,EAAE;QAC9D,MAAM,OAAO,GAAG,uBAAuB,CAAC,EAAE,CAAC,CAAA;QAC3C,MAAM,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,YAAY,EAAE,SAAS,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,SAAS,CAAC,CAAA;IAChF,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,43 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { execSync } from 'node:child_process';
3
+ import { mkdtempSync, writeFileSync } from 'node:fs';
4
+ import { readFile } from 'node:fs/promises';
5
+ import { tmpdir } from 'node:os';
6
+ import { dirname, join } from 'node:path';
7
+ import { fileURLToPath } from 'node:url';
8
+ import { runPipeline } from '../../pipeline.js';
9
+ import { resolveProductionKotlinEmitter } from './ajsc-adapter.js';
10
+ const __filename = fileURLToPath(import.meta.url);
11
+ const __dirname = dirname(__filename);
12
+ function kotlincAvailable() {
13
+ try {
14
+ execSync('kotlinc -version', { stdio: 'ignore' });
15
+ return true;
16
+ }
17
+ catch {
18
+ return false;
19
+ }
20
+ }
21
+ describe('kotlin codegen — kotlinc compile (gated)', () => {
22
+ it.skipIf(!kotlincAvailable() || process.env.TS_PROCEDURES_KOTLIN_E2E !== '1')('compiles generated output without errors', async () => {
23
+ const emitter = await resolveProductionKotlinEmitter();
24
+ const envelope = JSON.parse(await readFile(join(__dirname, '__fixtures__/users-envelope.json'), 'utf8'));
25
+ const files = await runPipeline({
26
+ envelope,
27
+ outDir: 'out',
28
+ dryRun: true,
29
+ target: 'kotlin',
30
+ kotlinPackage: 'com.example.api',
31
+ kotlinEmitter: emitter,
32
+ });
33
+ const dir = mkdtempSync(join(tmpdir(), 'tsp-kotlin-e2e-'));
34
+ for (const f of files) {
35
+ writeFileSync(join(dir, f.path.split('/').pop()), f.code);
36
+ }
37
+ // Compile against the kotlinx-serialization runtime jar present in the test env.
38
+ // If the jar isn't on the classpath, this fails; CI must provide it.
39
+ execSync(`kotlinc ${dir}/*.kt -d ${dir}/out.jar`, { stdio: 'inherit' });
40
+ expect(true).toBe(true); // reaching here means compile succeeded
41
+ });
42
+ });
43
+ //# sourceMappingURL=e2e-compile.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"e2e-compile.test.js","sourceRoot":"","sources":["../../../../src/codegen/targets/kotlin/e2e-compile.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AAC7C,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAA;AAC7C,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,SAAS,CAAA;AACpD,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAA;AAC3C,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAA;AAChC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AACzC,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AACxC,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAC/C,OAAO,EAAE,8BAA8B,EAAE,MAAM,mBAAmB,CAAA;AAElE,MAAM,UAAU,GAAG,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;AACjD,MAAM,SAAS,GAAG,OAAO,CAAC,UAAU,CAAC,CAAA;AAErC,SAAS,gBAAgB;IACvB,IAAI,CAAC;QACH,QAAQ,CAAC,kBAAkB,EAAE,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAC,CAAA;QACjD,OAAO,IAAI,CAAA;IACb,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAA;IACd,CAAC;AACH,CAAC;AAED,QAAQ,CAAC,0CAA0C,EAAE,GAAG,EAAE;IACxD,EAAE,CAAC,MAAM,CAAC,CAAC,gBAAgB,EAAE,IAAI,OAAO,CAAC,GAAG,CAAC,wBAAwB,KAAK,GAAG,CAAC,CAC5E,0CAA0C,EAC1C,KAAK,IAAI,EAAE;QACT,MAAM,OAAO,GAAG,MAAM,8BAA8B,EAAE,CAAA;QACtD,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CACzB,MAAM,QAAQ,CAAC,IAAI,CAAC,SAAS,EAAE,kCAAkC,CAAC,EAAE,MAAM,CAAC,CAC5E,CAAA;QACD,MAAM,KAAK,GAAG,MAAM,WAAW,CAAC;YAC9B,QAAQ;YACR,MAAM,EAAE,KAAK;YACb,MAAM,EAAE,IAAI;YACZ,MAAM,EAAE,QAAQ;YAChB,aAAa,EAAE,iBAAiB;YAChC,aAAa,EAAE,OAAO;SACvB,CAAC,CAAA;QACF,MAAM,GAAG,GAAG,WAAW,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,iBAAiB,CAAC,CAAC,CAAA;QAC1D,KAAK,MAAM,CAAC,IAAI,KAAK,EAAE,CAAC;YACtB,aAAa,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,EAAG,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,CAAA;QAC5D,CAAC;QACD,iFAAiF;QACjF,qEAAqE;QACrE,QAAQ,CAAC,WAAW,GAAG,YAAY,GAAG,UAAU,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC,CAAA;QACvE,MAAM,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA,CAAC,wCAAwC;IAClE,CAAC,CACF,CAAA;AACH,CAAC,CAAC,CAAA"}
@@ -0,0 +1,11 @@
1
+ import type { AnyHttpRouteDoc } from '../../../implementations/types.js';
2
+ import type { KotlinEmitter } from './ajsc-adapter.js';
3
+ export interface EmitRouteResult {
4
+ /** Inner body of the `object RouteName { ... }` block — already indented one level. */
5
+ code: string;
6
+ /** Imports collected from every ajsc emit + any helpers this route used. */
7
+ imports: string[];
8
+ /** Outer route name used as the `object RouteName` identifier. */
9
+ routeName: string;
10
+ }
11
+ export declare function emitKotlinRoute(route: AnyHttpRouteDoc, emitter: KotlinEmitter, errorSchemas: Map<string, unknown>): EmitRouteResult;
@@ -0,0 +1,73 @@
1
+ import { indent } from './format-kotlin.js';
2
+ const COLON_PARAM_RE = /:([A-Za-z_][A-Za-z0-9_]*)/g;
3
+ function toBracePath(template) {
4
+ return template.replace(COLON_PARAM_RE, '{$1}');
5
+ }
6
+ function pathParamNames(template) {
7
+ const names = [];
8
+ for (const match of template.matchAll(COLON_PARAM_RE))
9
+ names.push(match[1]);
10
+ return names;
11
+ }
12
+ function buildPathFn(bracePath, params) {
13
+ if (params.length === 0)
14
+ return `const val path = "${bracePath}"`;
15
+ let body = bracePath;
16
+ for (const name of params)
17
+ body = body.replace(`{${name}}`, `\${p.${name}}`);
18
+ return `fun path(p: PathParams): String = "${body}"`;
19
+ }
20
+ export function emitKotlinRoute(route, emitter, errorSchemas) {
21
+ const kind = route.kind;
22
+ if (kind === 'stream') {
23
+ console.warn(`[ts-procedures-codegen] Skipping stream route "${route.name}" — streams are out of scope for kotlin target.`);
24
+ return { code: '', imports: [], routeName: route.name };
25
+ }
26
+ const isApi = kind === 'api' || 'fullPath' in route;
27
+ const rawPath = isApi ? route.fullPath : route.path;
28
+ const method = String(route.method).toUpperCase();
29
+ const bracePath = toBracePath(rawPath);
30
+ const params = pathParamNames(rawPath);
31
+ const lines = [
32
+ `const val method = "${method}"`,
33
+ `const val pathTemplate = "${bracePath}"`,
34
+ buildPathFn(bracePath, params),
35
+ ];
36
+ const imports = [];
37
+ const schema = route.schema ?? {};
38
+ const input = (schema.input ?? {});
39
+ // Per-slot emission. Order is fixed for deterministic output.
40
+ const slots = [
41
+ { key: 'pathParams', rootName: 'PathParams', source: input.pathParams },
42
+ { key: 'query', rootName: 'Query', source: input.query },
43
+ { key: 'body', rootName: 'Body', source: input.body },
44
+ { key: 'response', rootName: 'Response', source: schema.returnType },
45
+ ];
46
+ for (const slot of slots) {
47
+ if (slot.source == null)
48
+ continue;
49
+ const result = emitter.emit(slot.source, { rootTypeName: slot.rootName });
50
+ lines.push('');
51
+ lines.push(result.code);
52
+ imports.push(...result.imports);
53
+ }
54
+ // Errors namespace — route.errors is `string[]` of taxonomy keys; look up each schema
55
+ // from the envelope-level errors map. Keys without schemas are skipped silently
56
+ // (matching the existing TS scope emitter's `errorKeys` filter).
57
+ const routeErrorKeys = (route.errors ?? [])
58
+ .filter((key) => errorSchemas.has(key));
59
+ if (routeErrorKeys.length > 0) {
60
+ const inner = [];
61
+ for (const key of routeErrorKeys) {
62
+ const r = emitter.emit(errorSchemas.get(key), { rootTypeName: key });
63
+ inner.push(r.code);
64
+ imports.push(...r.imports);
65
+ }
66
+ lines.push('');
67
+ lines.push('object Errors {');
68
+ lines.push(indent(inner.join('\n\n'), 1));
69
+ lines.push('}');
70
+ }
71
+ return { code: lines.join('\n'), imports, routeName: route.name };
72
+ }
73
+ //# sourceMappingURL=emit-route-kotlin.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"emit-route-kotlin.js","sourceRoot":"","sources":["../../../../src/codegen/targets/kotlin/emit-route-kotlin.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,88 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { emitKotlinRoute } from './emit-route-kotlin.js';
3
+ import { createStubKotlinEmitter } from './ajsc-adapter.js';
4
+ const ok = (code, rootTypeName) => ({
5
+ code,
6
+ rootTypeName,
7
+ extractedTypeNames: [],
8
+ imports: ['kotlinx.serialization.Serializable'],
9
+ });
10
+ const noErrors = new Map();
11
+ describe('emitKotlinRoute', () => {
12
+ it('emits an api-kind route with path params and a response', () => {
13
+ const route = {
14
+ kind: 'api',
15
+ name: 'GetUser',
16
+ method: 'GET',
17
+ fullPath: '/users/:id',
18
+ schema: {
19
+ input: {
20
+ pathParams: { type: 'object' },
21
+ },
22
+ returnType: { type: 'object' },
23
+ },
24
+ errors: [],
25
+ };
26
+ const emitter = createStubKotlinEmitter({
27
+ PathParams: ok('@Serializable data class PathParams(val id: String)', 'PathParams'),
28
+ Response: ok('@Serializable data class Response(val id: String, val name: String)', 'Response'),
29
+ });
30
+ const result = emitKotlinRoute(route, emitter, noErrors);
31
+ expect(result.imports).toContain('kotlinx.serialization.Serializable');
32
+ expect(result.code).toContain('const val method = "GET"');
33
+ expect(result.code).toContain('const val pathTemplate = "/users/{id}"');
34
+ expect(result.code).toContain('fun path(p: PathParams): String = "/users/${p.id}"');
35
+ expect(result.code).toContain('@Serializable data class PathParams(val id: String)');
36
+ expect(result.code).toContain('@Serializable data class Response(val id: String, val name: String)');
37
+ });
38
+ it('emits a route with no path params using a path constant', () => {
39
+ const route = {
40
+ kind: 'api',
41
+ name: 'CreateUser',
42
+ method: 'POST',
43
+ fullPath: '/users',
44
+ schema: { input: { body: { type: 'object' } }, returnType: { type: 'object' } },
45
+ errors: [],
46
+ };
47
+ const emitter = createStubKotlinEmitter({
48
+ Body: ok('@Serializable data class Body(val name: String)', 'Body'),
49
+ Response: ok('@Serializable data class Response(val id: String)', 'Response'),
50
+ });
51
+ const result = emitKotlinRoute(route, emitter, noErrors);
52
+ expect(result.code).toContain('const val path = "/users"');
53
+ expect(result.code).not.toContain('fun path(');
54
+ });
55
+ it('emits an Errors namespace for routes whose error keys have schemas in the envelope', () => {
56
+ const route = {
57
+ kind: 'api',
58
+ name: 'GetUser',
59
+ method: 'GET',
60
+ fullPath: '/users/:id',
61
+ schema: { input: { pathParams: { type: 'object' } }, returnType: { type: 'object' } },
62
+ errors: ['NotFound'],
63
+ };
64
+ const emitter = createStubKotlinEmitter({
65
+ PathParams: ok('@Serializable data class PathParams(val id: String)', 'PathParams'),
66
+ Response: ok('@Serializable data class Response(val id: String)', 'Response'),
67
+ NotFound: ok('@Serializable data class NotFound(val name: String, val message: String)', 'NotFound'),
68
+ });
69
+ const errorSchemas = new Map([['NotFound', { type: 'object' }]]);
70
+ const result = emitKotlinRoute(route, emitter, errorSchemas);
71
+ expect(result.code).toContain('object Errors {');
72
+ expect(result.code).toContain('@Serializable data class NotFound(val name: String, val message: String)');
73
+ });
74
+ it('silently skips error keys with no schema in the envelope map', () => {
75
+ const route = {
76
+ kind: 'api', name: 'GetUser', method: 'GET', fullPath: '/users',
77
+ schema: {}, errors: ['UnknownTaxonomyKey'],
78
+ };
79
+ const result = emitKotlinRoute(route, createStubKotlinEmitter({}), new Map());
80
+ expect(result.code).not.toContain('object Errors {');
81
+ });
82
+ it('skips stream routes with a warning', () => {
83
+ const route = { kind: 'stream', name: 'WatchUsers', method: 'GET', path: '/users/stream', schema: {}, errors: [] };
84
+ const result = emitKotlinRoute(route, createStubKotlinEmitter({}), noErrors);
85
+ expect(result.code).toBe('');
86
+ });
87
+ });
88
+ //# sourceMappingURL=emit-route-kotlin.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"emit-route-kotlin.test.js","sourceRoot":"","sources":["../../../../src/codegen/targets/kotlin/emit-route-kotlin.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,11 @@
1
+ import type { ScopeGroup } from '../../group-routes.js';
2
+ import type { KotlinEmitter } from './ajsc-adapter.js';
3
+ export interface EmitScopeOptions {
4
+ kotlinPackage: string;
5
+ sourceHash: string;
6
+ }
7
+ export interface EmittedKotlinFile {
8
+ filename: string;
9
+ code: string;
10
+ }
11
+ export declare function emitKotlinScope(group: ScopeGroup, opts: EmitScopeOptions, emitter: KotlinEmitter, errorSchemas: Map<string, unknown>): EmittedKotlinFile;
@@ -0,0 +1,35 @@
1
+ import { emitKotlinRoute } from './emit-route-kotlin.js';
2
+ import { kotlinPackageDecl, kotlinSourceHashHeader, kotlinImports, indent } from './format-kotlin.js';
3
+ function pascalCase(scope) {
4
+ return scope
5
+ .split('-')
6
+ .filter((p) => p.length > 0)
7
+ .map((p) => p.charAt(0).toUpperCase() + p.slice(1))
8
+ .join('');
9
+ }
10
+ export function emitKotlinScope(group, opts, emitter, errorSchemas) {
11
+ const scopeName = pascalCase(group.scopeKey);
12
+ const allImports = [];
13
+ const routeBlocks = [];
14
+ for (const route of group.routes) {
15
+ const r = emitKotlinRoute(route, emitter, errorSchemas);
16
+ if (r.code === '')
17
+ continue;
18
+ allImports.push(...r.imports);
19
+ const wrapped = `object ${r.routeName} {\n${indent(r.code, 1)}\n}`;
20
+ routeBlocks.push(wrapped);
21
+ }
22
+ const innerScope = routeBlocks.length === 0 ? '' : indent(routeBlocks.join('\n\n'), 1);
23
+ const scopeBlock = innerScope === ''
24
+ ? `object ${scopeName} {\n}`
25
+ : `object ${scopeName} {\n${innerScope}\n}`;
26
+ const importsBlock = kotlinImports(allImports);
27
+ const parts = [
28
+ kotlinPackageDecl(opts.kotlinPackage),
29
+ kotlinSourceHashHeader(opts.sourceHash),
30
+ importsBlock,
31
+ scopeBlock,
32
+ ].filter((p) => p.length > 0);
33
+ return { filename: `${scopeName}.kt`, code: parts.join('\n\n') + '\n' };
34
+ }
35
+ //# sourceMappingURL=emit-scope-kotlin.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"emit-scope-kotlin.js","sourceRoot":"","sources":["../../../../src/codegen/targets/kotlin/emit-scope-kotlin.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAA;AACxD,OAAO,EAAE,iBAAiB,EAAE,sBAAsB,EAAE,aAAa,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAA;AAYrG,SAAS,UAAU,CAAC,KAAa;IAC/B,OAAO,KAAK;SACT,KAAK,CAAC,GAAG,CAAC;SACV,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC;SAC3B,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;SAClD,IAAI,CAAC,EAAE,CAAC,CAAA;AACb,CAAC;AAED,MAAM,UAAU,eAAe,CAC7B,KAAiB,EACjB,IAAsB,EACtB,OAAsB,EACtB,YAAkC;IAElC,MAAM,SAAS,GAAG,UAAU,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAA;IAC5C,MAAM,UAAU,GAAa,EAAE,CAAA;IAC/B,MAAM,WAAW,GAAa,EAAE,CAAA;IAEhC,KAAK,MAAM,KAAK,IAAI,KAAK,CAAC,MAAM,EAAE,CAAC;QACjC,MAAM,CAAC,GAAG,eAAe,CAAC,KAAK,EAAE,OAAO,EAAE,YAAY,CAAC,CAAA;QACvD,IAAI,CAAC,CAAC,IAAI,KAAK,EAAE;YAAE,SAAQ;QAC3B,UAAU,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,CAAA;QAC7B,MAAM,OAAO,GAAG,UAAU,CAAC,CAAC,SAAS,OAAO,MAAM,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,KAAK,CAAA;QAClE,WAAW,CAAC,IAAI,CAAC,OAAO,CAAC,CAAA;IAC3B,CAAC;IAED,MAAM,UAAU,GAAG,WAAW,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,WAAW,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,CAAA;IACtF,MAAM,UAAU,GAAG,UAAU,KAAK,EAAE;QAClC,CAAC,CAAC,UAAU,SAAS,OAAO;QAC5B,CAAC,CAAC,UAAU,SAAS,OAAO,UAAU,KAAK,CAAA;IAE7C,MAAM,YAAY,GAAG,aAAa,CAAC,UAAU,CAAC,CAAA;IAC9C,MAAM,KAAK,GAAG;QACZ,iBAAiB,CAAC,IAAI,CAAC,aAAa,CAAC;QACrC,sBAAsB,CAAC,IAAI,CAAC,UAAU,CAAC;QACvC,YAAY;QACZ,UAAU;KACX,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAA;IAE7B,OAAO,EAAE,QAAQ,EAAE,GAAG,SAAS,KAAK,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,IAAI,EAAE,CAAA;AACzE,CAAC"}