@mandujs/core 0.41.2 → 0.43.0

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 (88) hide show
  1. package/package.json +21 -4
  2. package/src/auth/__tests__/login.test.ts +420 -419
  3. package/src/auth/__tests__/reset.test.ts +296 -296
  4. package/src/brain/adapters/anthropic-oauth.ts +421 -420
  5. package/src/brain/adapters/index.ts +2 -1
  6. package/src/brain/adapters/ollama.ts +1 -1
  7. package/src/brain/adapters/openai-oauth.ts +534 -533
  8. package/src/brain/brain.ts +2 -1
  9. package/src/brain/redactor.ts +196 -196
  10. package/src/bundler/__tests__/cli-bench-utils.test.ts +149 -149
  11. package/src/bundler/__tests__/cold-start.test.ts +504 -504
  12. package/src/bundler/__tests__/fast-refresh.test.ts +607 -606
  13. package/src/bundler/__tests__/hdr.test.ts +1 -1
  14. package/src/bundler/analyzer.ts +958 -958
  15. package/src/bundler/build.ts +104 -14
  16. package/src/bundler/dev.ts +125 -0
  17. package/src/bundler/hmr-types.ts +1 -0
  18. package/src/bundler/plugins/__tests__/react-compiler-lint.test.ts +110 -0
  19. package/src/bundler/plugins/index.ts +14 -0
  20. package/src/bundler/plugins/react-compiler-lint.ts +253 -0
  21. package/src/bundler/plugins/react-compiler.ts +162 -0
  22. package/src/bundler/types.ts +12 -0
  23. package/src/change/integrity.ts +2 -1
  24. package/src/client/index.ts +10 -0
  25. package/src/client/island.ts +38 -11
  26. package/src/client/router.ts +6 -1
  27. package/src/config/mandu.ts +57 -0
  28. package/src/config/validate.ts +42 -0
  29. package/src/content/collection.ts +844 -809
  30. package/src/content/content-layer.ts +316 -314
  31. package/src/content/content.test.ts +433 -433
  32. package/src/content/digest.ts +133 -133
  33. package/src/content/generate-types.ts +168 -168
  34. package/src/content/index.ts +6 -1
  35. package/src/content/llms-txt.ts +277 -277
  36. package/src/contract/define.ts +474 -474
  37. package/src/contract/route-helpers.ts +2 -1
  38. package/src/contract/zod-utils.ts +158 -155
  39. package/src/db/index.ts +513 -513
  40. package/src/desktop/__tests__/smoke.test.ts +100 -100
  41. package/src/desktop/webview-fallback.ts +583 -583
  42. package/src/desktop/window.ts +3 -1
  43. package/src/dev-error-overlay/overlay-client.ts +300 -300
  44. package/src/devtools/ai/mcp-connector.ts +499 -498
  45. package/src/devtools/client/components/kitchen-root.tsx +7 -2
  46. package/src/email/resend.ts +163 -163
  47. package/src/guard/__tests__/tsgolint-bridge.test.ts +347 -0
  48. package/src/guard/ast-analyzer.ts +806 -806
  49. package/src/guard/graph.ts +898 -898
  50. package/src/guard/index.ts +16 -0
  51. package/src/guard/statistics.ts +578 -578
  52. package/src/guard/tsgolint-bridge.ts +512 -0
  53. package/src/i18n/locale-resolver.ts +214 -214
  54. package/src/id/__tests__/id.test.ts +120 -120
  55. package/src/intent/index.ts +321 -321
  56. package/src/island/index.ts +39 -23
  57. package/src/kitchen/api/contract-api.ts +15 -8
  58. package/src/kitchen/kitchen-ui.ts +2137 -2137
  59. package/src/lockfile/index.ts +3 -2
  60. package/src/middleware/oauth/__tests__/oauth.test.ts +575 -574
  61. package/src/middleware/rate-limit/__tests__/rate-limit.test.ts +642 -642
  62. package/src/middleware/secure/index.ts +417 -417
  63. package/src/observability/event-bus.ts +2 -2
  64. package/src/observability/metrics.ts +334 -334
  65. package/src/observability/tracing.ts +694 -694
  66. package/src/openapi/generator.ts +1 -1
  67. package/src/perf/user-marks.ts +553 -553
  68. package/src/plugins/registry.ts +387 -387
  69. package/src/resource/ddl/diff.ts +392 -392
  70. package/src/resource/ddl/snapshot.ts +448 -447
  71. package/src/resource/generator-schema.ts +477 -476
  72. package/src/resource/parser.ts +4 -2
  73. package/src/resource/schema.ts +1 -1
  74. package/src/router/fs-patterns.ts +422 -422
  75. package/src/runtime/fast-refresh-types.ts +126 -128
  76. package/src/runtime/image-handler.ts +206 -195
  77. package/src/runtime/router.test.ts +476 -476
  78. package/src/runtime/security.ts +155 -155
  79. package/src/runtime/server.ts +36 -19
  80. package/src/runtime/session-key.ts +328 -328
  81. package/src/scheduler/__tests__/scheduler.test.ts +514 -514
  82. package/src/seo/resolve/index.ts +353 -353
  83. package/src/spec/load.ts +1 -1
  84. package/src/testing/reporter.ts +676 -676
  85. package/src/testing/server.ts +196 -196
  86. package/src/testing/snapshot.ts +444 -444
  87. package/src/utils/__tests__/lru-cache.test.ts +186 -186
  88. package/src/utils/bun.ts +8 -8
@@ -1,321 +1,321 @@
1
- /**
2
- * Mandu Intent - 의도 기반 API 라우팅
3
- *
4
- * @example
5
- * ```ts
6
- * import { intent } from '@mandujs/core';
7
- *
8
- * export default intent({
9
- * '사용자 목록 조회': {
10
- * method: 'GET',
11
- * handler: (ctx) => ctx.ok(users),
12
- * },
13
- * '사용자 생성': {
14
- * method: 'POST',
15
- * input: z.object({ name: z.string() }),
16
- * handler: async (ctx) => {
17
- * const data = await ctx.body();
18
- * return ctx.created(createUser(data));
19
- * },
20
- * },
21
- * });
22
- * ```
23
- */
24
-
25
- import { z, type ZodType } from 'zod';
26
- import { ManduFillingFactory, type ManduFilling } from '../filling/filling';
27
- import type { ManduContext } from '../filling/context';
28
- import { getZodTypeName, getZodObjectShape, getZodArrayElementType } from '../contract/zod-utils';
29
- import type { ZodTypeAny } from 'zod';
30
-
31
- // ============================================================================
32
- // Types
33
- // ============================================================================
34
-
35
- export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'OPTIONS';
36
-
37
- export interface IntentDefinition<TInput = unknown, TOutput = unknown> {
38
- /** HTTP 메서드 */
39
- method: HttpMethod;
40
- /** 추가 경로 (예: '/:id') */
41
- path?: string;
42
- /** 입력 스키마 (Zod) */
43
- input?: ZodType<TInput>;
44
- /** 출력 스키마 (Zod) - 문서화/검증용 */
45
- output?: ZodType<TOutput>;
46
- /** 가능한 에러 코드 목록 */
47
- errors?: readonly string[];
48
- /** 설명 (OpenAPI 문서용) */
49
- description?: string;
50
- /** 핸들러 함수 */
51
- handler: (ctx: ManduContext) => Response | Promise<Response>;
52
- /** Guard/Middleware */
53
- guard?: (ctx: ManduContext) => Response | void | Promise<Response | void>;
54
- }
55
-
56
- export type IntentMap = Record<string, IntentDefinition<unknown, unknown>>;
57
-
58
- export interface IntentMeta {
59
- __intent: true;
60
- __intents: IntentMap;
61
- __docs: IntentDocumentation[];
62
- }
63
-
64
- export interface IntentDocumentation {
65
- name: string;
66
- method: HttpMethod;
67
- path: string;
68
- description?: string;
69
- input?: ZodType<unknown>;
70
- output?: ZodType<unknown>;
71
- errors?: readonly string[];
72
- }
73
-
74
- // ============================================================================
75
- // intent() - 의도 기반 API 생성
76
- // ============================================================================
77
-
78
- /**
79
- * 의도 기반 API 라우트 생성
80
- *
81
- * 하나의 파일에서 여러 관련 API를 의도(intent)로 그룹화
82
- * - 의도 이름이 자동으로 OpenAPI description이 됨
83
- * - AI가 "사용자 삭제 API"를 쉽게 찾을 수 있음
84
- * - 타입 안전한 입출력
85
- */
86
- export function intent(intents: IntentMap): ManduFilling & IntentMeta {
87
- const filling = ManduFillingFactory.filling();
88
- const docs: IntentDocumentation[] = [];
89
-
90
- // 메서드별로 핸들러 그룹화
91
- const methodHandlers: Record<HttpMethod, IntentDefinition<unknown, unknown>[]> = {
92
- GET: [],
93
- POST: [],
94
- PUT: [],
95
- PATCH: [],
96
- DELETE: [],
97
- HEAD: [],
98
- OPTIONS: [],
99
- };
100
-
101
- // Intent 분류 및 문서화
102
- for (const [intentName, definition] of Object.entries(intents)) {
103
- methodHandlers[definition.method].push({
104
- ...definition,
105
- description: definition.description || intentName,
106
- });
107
-
108
- docs.push({
109
- name: intentName,
110
- method: definition.method,
111
- path: definition.path || '/',
112
- description: definition.description || intentName,
113
- input: definition.input,
114
- output: definition.output,
115
- errors: definition.errors,
116
- });
117
- }
118
-
119
- // 각 메서드에 대해 핸들러 등록
120
- const registerMethod = (method: HttpMethod, handlers: IntentDefinition<unknown, unknown>[]) => {
121
- if (handlers.length === 0) return;
122
-
123
- const methodLower = method.toLowerCase() as Lowercase<HttpMethod>;
124
- const fillingWithMethods = filling as ManduFilling & Record<Lowercase<HttpMethod>, (handler: (ctx: ManduContext) => Response | Promise<Response>) => ManduFilling>;
125
-
126
- fillingWithMethods[methodLower](async (ctx: ManduContext) => {
127
- // 경로 매칭 (path가 있는 경우)
128
- for (const def of handlers) {
129
- if (def.path && !matchPath(ctx.url, def.path)) {
130
- continue;
131
- }
132
-
133
- // Guard 실행
134
- if (def.guard) {
135
- const guardResult = await def.guard(ctx);
136
- if (guardResult instanceof Response) {
137
- return guardResult;
138
- }
139
- }
140
-
141
- // Input 검증 (ctx.body() throws ValidationError on schema mismatch)
142
- if (def.input && ['POST', 'PUT', 'PATCH'].includes(method)) {
143
- try {
144
- await ctx.body(def.input);
145
- } catch (validationError) {
146
- return ctx.error(
147
- 'Validation failed',
148
- validationError instanceof Error ? validationError : new Error(String(validationError))
149
- );
150
- }
151
- }
152
-
153
- // 핸들러 실행
154
- return def.handler(ctx);
155
- }
156
-
157
- // 매칭되는 핸들러 없음
158
- return ctx.notFound(`No handler for ${method} ${ctx.url}`);
159
- });
160
- };
161
-
162
- // 모든 메서드 등록
163
- for (const [method, handlers] of Object.entries(methodHandlers)) {
164
- registerMethod(method as HttpMethod, handlers);
165
- }
166
-
167
- // 메타데이터 부착
168
- const result = filling as ManduFilling & IntentMeta;
169
- result.__intent = true;
170
- result.__intents = intents;
171
- result.__docs = docs;
172
-
173
- return result;
174
- }
175
-
176
- // ============================================================================
177
- // Helper Functions
178
- // ============================================================================
179
-
180
- /**
181
- * 간단한 경로 매칭 (/:id 같은 패턴)
182
- */
183
- function matchPath(url: string, pattern: string): boolean {
184
- if (pattern === '/') return true;
185
-
186
- const urlPath = new URL(url, 'http://localhost').pathname;
187
- const patternParts = pattern.split('/').filter(Boolean);
188
- const urlParts = urlPath.split('/').filter(Boolean);
189
-
190
- if (patternParts.length !== urlParts.length) return false;
191
-
192
- return patternParts.every((part, i) => {
193
- if (part.startsWith(':')) return true; // 동적 파라미터
194
- return part === urlParts[i];
195
- });
196
- }
197
-
198
- /**
199
- * Intent에서 OpenAPI 스펙 생성
200
- */
201
- export function generateOpenAPIFromIntent(
202
- basePath: string,
203
- intentFilling: IntentMeta
204
- ): Record<string, unknown> {
205
- const paths: Record<string, Record<string, unknown>> = {};
206
-
207
- for (const doc of intentFilling.__docs) {
208
- const fullPath = basePath + (doc.path === '/' ? '' : doc.path);
209
- const method = doc.method.toLowerCase();
210
-
211
- if (!paths[fullPath]) {
212
- paths[fullPath] = {};
213
- }
214
-
215
- paths[fullPath][method] = {
216
- summary: doc.name,
217
- description: doc.description,
218
- requestBody: doc.input
219
- ? {
220
- content: {
221
- 'application/json': {
222
- schema: zodToJsonSchema(doc.input),
223
- },
224
- },
225
- }
226
- : undefined,
227
- responses: {
228
- '200': {
229
- description: 'Success',
230
- content: doc.output
231
- ? {
232
- 'application/json': {
233
- schema: zodToJsonSchema(doc.output),
234
- },
235
- }
236
- : undefined,
237
- },
238
- ...(doc.errors?.reduce(
239
- (acc, error) => ({
240
- ...acc,
241
- [getErrorStatusCode(error)]: { description: error },
242
- }),
243
- {}
244
- ) || {}),
245
- },
246
- };
247
- }
248
-
249
- return { paths };
250
- }
251
-
252
- /**
253
- * 간단한 Zod → JSON Schema 변환
254
- */
255
- function zodToJsonSchema(schema: ZodType<unknown>): Record<string, unknown> {
256
- // 실제 구현은 zod-to-json-schema 라이브러리 사용 권장
257
- const typeName = getZodTypeName(schema as ZodTypeAny);
258
-
259
- if (typeName === 'ZodString') {
260
- return { type: 'string' };
261
- }
262
- if (typeName === 'ZodNumber') {
263
- return { type: 'number' };
264
- }
265
- if (typeName === 'ZodBoolean') {
266
- return { type: 'boolean' };
267
- }
268
- if (typeName === 'ZodObject') {
269
- const properties: Record<string, unknown> = {};
270
- const shape = getZodObjectShape(schema as ZodTypeAny);
271
- if (shape) {
272
- for (const [key, value] of Object.entries(shape)) {
273
- properties[key] = zodToJsonSchema(value as ZodType<unknown>);
274
- }
275
- }
276
- return { type: 'object', properties };
277
- }
278
- if (typeName === 'ZodArray') {
279
- const elementType = getZodArrayElementType(schema as ZodTypeAny);
280
- return { type: 'array', items: elementType ? zodToJsonSchema(elementType as ZodType<unknown>) : {} };
281
- }
282
-
283
- return { type: 'unknown' };
284
- }
285
-
286
- /**
287
- * 에러 코드 → HTTP 상태 코드
288
- */
289
- function getErrorStatusCode(error: string): number {
290
- const errorMap: Record<string, number> = {
291
- NOT_FOUND: 404,
292
- UNAUTHORIZED: 401,
293
- FORBIDDEN: 403,
294
- RATE_LIMITED: 429,
295
- INVALID_INPUT: 400,
296
- VALIDATION_ERROR: 400,
297
- INTERNAL_ERROR: 500,
298
- };
299
-
300
- return errorMap[error] || 400;
301
- }
302
-
303
- // ============================================================================
304
- // isIntent() - Intent 체크
305
- // ============================================================================
306
-
307
- export function isIntent(value: unknown): value is ManduFilling & IntentMeta {
308
- return (
309
- typeof value === 'object' &&
310
- value !== null &&
311
- (value as IntentMeta).__intent === true
312
- );
313
- }
314
-
315
- // ============================================================================
316
- // getIntentDocs() - Intent 문서 추출
317
- // ============================================================================
318
-
319
- export function getIntentDocs(intentFilling: IntentMeta): IntentDocumentation[] {
320
- return intentFilling.__docs;
321
- }
1
+ /**
2
+ * Mandu Intent - 의도 기반 API 라우팅
3
+ *
4
+ * @example
5
+ * ```ts
6
+ * import { intent } from '@mandujs/core';
7
+ *
8
+ * export default intent({
9
+ * '사용자 목록 조회': {
10
+ * method: 'GET',
11
+ * handler: (ctx) => ctx.ok(users),
12
+ * },
13
+ * '사용자 생성': {
14
+ * method: 'POST',
15
+ * input: z.object({ name: z.string() }),
16
+ * handler: async (ctx) => {
17
+ * const data = await ctx.body();
18
+ * return ctx.created(createUser(data));
19
+ * },
20
+ * },
21
+ * });
22
+ * ```
23
+ */
24
+
25
+ import { z, type ZodType } from 'zod';
26
+ import { ManduFillingFactory, type ManduFilling } from '../filling/filling';
27
+ import type { ManduContext } from '../filling/context';
28
+ import { getZodTypeName, getZodObjectShape, getZodArrayElementType } from '../contract/zod-utils';
29
+ import type { ZodTypeAny } from 'zod';
30
+
31
+ // ============================================================================
32
+ // Types
33
+ // ============================================================================
34
+
35
+ export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'OPTIONS';
36
+
37
+ export interface IntentDefinition<TInput = unknown, TOutput = unknown> {
38
+ /** HTTP 메서드 */
39
+ method: HttpMethod;
40
+ /** 추가 경로 (예: '/:id') */
41
+ path?: string;
42
+ /** 입력 스키마 (Zod) */
43
+ input?: ZodType<TInput>;
44
+ /** 출력 스키마 (Zod) - 문서화/검증용 */
45
+ output?: ZodType<TOutput>;
46
+ /** 가능한 에러 코드 목록 */
47
+ errors?: readonly string[];
48
+ /** 설명 (OpenAPI 문서용) */
49
+ description?: string;
50
+ /** 핸들러 함수 */
51
+ handler: (ctx: ManduContext) => Response | Promise<Response>;
52
+ /** Guard/Middleware */
53
+ guard?: (ctx: ManduContext) => Response | void | Promise<Response | void>;
54
+ }
55
+
56
+ export type IntentMap = Record<string, IntentDefinition<unknown, unknown>>;
57
+
58
+ export interface IntentMeta {
59
+ __intent: true;
60
+ __intents: IntentMap;
61
+ __docs: IntentDocumentation[];
62
+ }
63
+
64
+ export interface IntentDocumentation {
65
+ name: string;
66
+ method: HttpMethod;
67
+ path: string;
68
+ description?: string;
69
+ input?: ZodType<unknown>;
70
+ output?: ZodType<unknown>;
71
+ errors?: readonly string[];
72
+ }
73
+
74
+ // ============================================================================
75
+ // intent() - 의도 기반 API 생성
76
+ // ============================================================================
77
+
78
+ /**
79
+ * 의도 기반 API 라우트 생성
80
+ *
81
+ * 하나의 파일에서 여러 관련 API를 의도(intent)로 그룹화
82
+ * - 의도 이름이 자동으로 OpenAPI description이 됨
83
+ * - AI가 "사용자 삭제 API"를 쉽게 찾을 수 있음
84
+ * - 타입 안전한 입출력
85
+ */
86
+ export function intent(intents: IntentMap): ManduFilling & IntentMeta {
87
+ const filling = ManduFillingFactory.filling();
88
+ const docs: IntentDocumentation[] = [];
89
+
90
+ // 메서드별로 핸들러 그룹화
91
+ const methodHandlers: Record<HttpMethod, IntentDefinition<unknown, unknown>[]> = {
92
+ GET: [],
93
+ POST: [],
94
+ PUT: [],
95
+ PATCH: [],
96
+ DELETE: [],
97
+ HEAD: [],
98
+ OPTIONS: [],
99
+ };
100
+
101
+ // Intent 분류 및 문서화
102
+ for (const [intentName, definition] of Object.entries(intents)) {
103
+ methodHandlers[definition.method].push({
104
+ ...definition,
105
+ description: definition.description || intentName,
106
+ });
107
+
108
+ docs.push({
109
+ name: intentName,
110
+ method: definition.method,
111
+ path: definition.path || '/',
112
+ description: definition.description || intentName,
113
+ input: definition.input,
114
+ output: definition.output,
115
+ errors: definition.errors,
116
+ });
117
+ }
118
+
119
+ // 각 메서드에 대해 핸들러 등록
120
+ const registerMethod = (method: HttpMethod, handlers: IntentDefinition<unknown, unknown>[]) => {
121
+ if (handlers.length === 0) return;
122
+
123
+ const methodLower = method.toLowerCase() as Lowercase<HttpMethod>;
124
+ const fillingWithMethods = filling as ManduFilling & Record<Lowercase<HttpMethod>, (handler: (ctx: ManduContext) => Response | Promise<Response>) => ManduFilling>;
125
+
126
+ fillingWithMethods[methodLower](async (ctx: ManduContext) => {
127
+ // 경로 매칭 (path가 있는 경우)
128
+ for (const def of handlers) {
129
+ if (def.path && !matchPath(ctx.url, def.path)) {
130
+ continue;
131
+ }
132
+
133
+ // Guard 실행
134
+ if (def.guard) {
135
+ const guardResult = await def.guard(ctx);
136
+ if (guardResult instanceof Response) {
137
+ return guardResult;
138
+ }
139
+ }
140
+
141
+ // Input 검증 (ctx.body() throws ValidationError on schema mismatch)
142
+ if (def.input && ['POST', 'PUT', 'PATCH'].includes(method)) {
143
+ try {
144
+ await ctx.body(def.input);
145
+ } catch (validationError) {
146
+ return ctx.error(
147
+ 'Validation failed',
148
+ validationError instanceof Error ? validationError : new Error(String(validationError))
149
+ );
150
+ }
151
+ }
152
+
153
+ // 핸들러 실행
154
+ return def.handler(ctx);
155
+ }
156
+
157
+ // 매칭되는 핸들러 없음
158
+ return ctx.notFound(`No handler for ${method} ${ctx.url}`);
159
+ });
160
+ };
161
+
162
+ // 모든 메서드 등록
163
+ for (const [method, handlers] of Object.entries(methodHandlers)) {
164
+ registerMethod(method as HttpMethod, handlers);
165
+ }
166
+
167
+ // 메타데이터 부착
168
+ const result = filling as ManduFilling & IntentMeta;
169
+ result.__intent = true;
170
+ result.__intents = intents;
171
+ result.__docs = docs;
172
+
173
+ return result;
174
+ }
175
+
176
+ // ============================================================================
177
+ // Helper Functions
178
+ // ============================================================================
179
+
180
+ /**
181
+ * 간단한 경로 매칭 (/:id 같은 패턴)
182
+ */
183
+ function matchPath(url: string, pattern: string): boolean {
184
+ if (pattern === '/') return true;
185
+
186
+ const urlPath = new URL(url, 'http://localhost').pathname;
187
+ const patternParts = pattern.split('/').filter(Boolean);
188
+ const urlParts = urlPath.split('/').filter(Boolean);
189
+
190
+ if (patternParts.length !== urlParts.length) return false;
191
+
192
+ return patternParts.every((part, i) => {
193
+ if (part.startsWith(':')) return true; // 동적 파라미터
194
+ return part === urlParts[i];
195
+ });
196
+ }
197
+
198
+ /**
199
+ * Intent에서 OpenAPI 스펙 생성
200
+ */
201
+ export function generateOpenAPIFromIntent(
202
+ basePath: string,
203
+ intentFilling: IntentMeta
204
+ ): Record<string, unknown> {
205
+ const paths: Record<string, Record<string, unknown>> = {};
206
+
207
+ for (const doc of intentFilling.__docs) {
208
+ const fullPath = basePath + (doc.path === '/' ? '' : doc.path);
209
+ const method = doc.method.toLowerCase();
210
+
211
+ if (!paths[fullPath]) {
212
+ paths[fullPath] = {};
213
+ }
214
+
215
+ paths[fullPath][method] = {
216
+ summary: doc.name,
217
+ description: doc.description,
218
+ requestBody: doc.input
219
+ ? {
220
+ content: {
221
+ 'application/json': {
222
+ schema: zodToJsonSchema(doc.input),
223
+ },
224
+ },
225
+ }
226
+ : undefined,
227
+ responses: {
228
+ '200': {
229
+ description: 'Success',
230
+ content: doc.output
231
+ ? {
232
+ 'application/json': {
233
+ schema: zodToJsonSchema(doc.output),
234
+ },
235
+ }
236
+ : undefined,
237
+ },
238
+ ...(doc.errors?.reduce(
239
+ (acc, error) => ({
240
+ ...acc,
241
+ [getErrorStatusCode(error)]: { description: error },
242
+ }),
243
+ {}
244
+ ) || {}),
245
+ },
246
+ };
247
+ }
248
+
249
+ return { paths };
250
+ }
251
+
252
+ /**
253
+ * 간단한 Zod → JSON Schema 변환
254
+ */
255
+ function zodToJsonSchema(schema: ZodType<unknown>): Record<string, unknown> {
256
+ // 실제 구현은 zod-to-json-schema 라이브러리 사용 권장
257
+ const typeName = getZodTypeName(schema as ZodTypeAny);
258
+
259
+ if (typeName === 'ZodString') {
260
+ return { type: 'string' };
261
+ }
262
+ if (typeName === 'ZodNumber') {
263
+ return { type: 'number' };
264
+ }
265
+ if (typeName === 'ZodBoolean') {
266
+ return { type: 'boolean' };
267
+ }
268
+ if (typeName === 'ZodObject') {
269
+ const properties: Record<string, unknown> = {};
270
+ const shape = getZodObjectShape(schema as ZodTypeAny);
271
+ if (shape) {
272
+ for (const [key, value] of Object.entries(shape)) {
273
+ properties[key] = zodToJsonSchema(value as ZodType<unknown>);
274
+ }
275
+ }
276
+ return { type: 'object', properties };
277
+ }
278
+ if (typeName === 'ZodArray') {
279
+ const elementType = getZodArrayElementType(schema as ZodTypeAny);
280
+ return { type: 'array', items: elementType ? zodToJsonSchema(elementType as ZodType<unknown>) : {} };
281
+ }
282
+
283
+ return { type: 'unknown' };
284
+ }
285
+
286
+ /**
287
+ * 에러 코드 → HTTP 상태 코드
288
+ */
289
+ function getErrorStatusCode(error: string): number {
290
+ const errorMap: Record<string, number> = {
291
+ NOT_FOUND: 404,
292
+ UNAUTHORIZED: 401,
293
+ FORBIDDEN: 403,
294
+ RATE_LIMITED: 429,
295
+ INVALID_INPUT: 400,
296
+ VALIDATION_ERROR: 400,
297
+ INTERNAL_ERROR: 500,
298
+ };
299
+
300
+ return errorMap[error] || 400;
301
+ }
302
+
303
+ // ============================================================================
304
+ // isIntent() - Intent 체크
305
+ // ============================================================================
306
+
307
+ export function isIntent(value: unknown): value is ManduFilling & IntentMeta {
308
+ return (
309
+ typeof value === 'object' &&
310
+ value !== null &&
311
+ (value as IntentMeta).__intent === true
312
+ );
313
+ }
314
+
315
+ // ============================================================================
316
+ // getIntentDocs() - Intent 문서 추출
317
+ // ============================================================================
318
+
319
+ export function getIntentDocs(intentFilling: IntentMeta): IntentDocumentation[] {
320
+ return intentFilling.__docs;
321
+ }