@mandujs/core 0.9.46 → 0.11.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 (56) hide show
  1. package/README.md +79 -10
  2. package/package.json +1 -1
  3. package/src/brain/doctor/config-analyzer.ts +498 -0
  4. package/src/brain/doctor/index.ts +10 -0
  5. package/src/change/snapshot.ts +46 -1
  6. package/src/change/types.ts +13 -0
  7. package/src/config/index.ts +9 -2
  8. package/src/config/mcp-ref.ts +348 -0
  9. package/src/config/mcp-status.ts +348 -0
  10. package/src/config/metadata.test.ts +308 -0
  11. package/src/config/metadata.ts +293 -0
  12. package/src/config/symbols.ts +144 -0
  13. package/src/config/validate.ts +122 -65
  14. package/src/config/watcher.ts +311 -0
  15. package/src/contract/index.ts +26 -25
  16. package/src/contract/protection.ts +364 -0
  17. package/src/error/domains.ts +265 -0
  18. package/src/error/index.ts +25 -13
  19. package/src/errors/extractor.ts +409 -0
  20. package/src/errors/index.ts +19 -0
  21. package/src/filling/context.ts +29 -1
  22. package/src/filling/deps.ts +238 -0
  23. package/src/filling/filling.ts +94 -8
  24. package/src/filling/index.ts +18 -0
  25. package/src/guard/analyzer.ts +7 -2
  26. package/src/guard/config-guard.ts +281 -0
  27. package/src/guard/decision-memory.test.ts +293 -0
  28. package/src/guard/decision-memory.ts +532 -0
  29. package/src/guard/healing.test.ts +259 -0
  30. package/src/guard/healing.ts +874 -0
  31. package/src/guard/index.ts +119 -0
  32. package/src/guard/negotiation.test.ts +282 -0
  33. package/src/guard/negotiation.ts +975 -0
  34. package/src/guard/semantic-slots.test.ts +379 -0
  35. package/src/guard/semantic-slots.ts +796 -0
  36. package/src/index.ts +4 -1
  37. package/src/lockfile/generate.ts +259 -0
  38. package/src/lockfile/index.ts +186 -0
  39. package/src/lockfile/lockfile.test.ts +410 -0
  40. package/src/lockfile/types.ts +184 -0
  41. package/src/lockfile/validate.ts +308 -0
  42. package/src/logging/index.ts +22 -0
  43. package/src/logging/transports.ts +365 -0
  44. package/src/plugins/index.ts +38 -0
  45. package/src/plugins/registry.ts +377 -0
  46. package/src/plugins/types.ts +363 -0
  47. package/src/runtime/security.ts +155 -0
  48. package/src/runtime/server.ts +318 -256
  49. package/src/runtime/session-key.ts +328 -0
  50. package/src/utils/differ.test.ts +342 -0
  51. package/src/utils/differ.ts +482 -0
  52. package/src/utils/hasher.test.ts +326 -0
  53. package/src/utils/hasher.ts +319 -0
  54. package/src/utils/index.ts +29 -0
  55. package/src/utils/safe-io.ts +188 -0
  56. package/src/utils/string-safe.ts +298 -0
@@ -0,0 +1,364 @@
1
+ /**
2
+ * Contract Protection - 보호 필드 시스템
3
+ *
4
+ * Symbol 메타데이터를 사용하여 Contract의 민감/보호 필드를 관리
5
+ *
6
+ * @see docs/plans/09_lockfile_integration_plan.md
7
+ */
8
+
9
+ import { z } from "zod";
10
+ import {
11
+ isSensitiveField,
12
+ isProtectedField,
13
+ getMetadata,
14
+ PROTECTED_FIELD,
15
+ SENSITIVE_FIELD,
16
+ type ProtectedFieldMetadata,
17
+ type SensitiveFieldMetadata,
18
+ } from "../config";
19
+
20
+ // ============================================
21
+ // 타입
22
+ // ============================================
23
+
24
+ export interface ProtectedFieldInfo {
25
+ /** 필드 경로 (예: "body.password") */
26
+ path: string;
27
+ /** 보호 이유 */
28
+ reason: string;
29
+ /** 수정 허용 대상 */
30
+ allowedModifiers: string[];
31
+ /** 민감 필드 여부 */
32
+ isSensitive: boolean;
33
+ }
34
+
35
+ export interface ProtectionViolation {
36
+ /** 위반 필드 경로 */
37
+ field: string;
38
+ /** 보호 이유 */
39
+ reason: string;
40
+ /** 오류 메시지 */
41
+ message: string;
42
+ /** 수정자 */
43
+ modifier: string;
44
+ }
45
+
46
+ export interface ContractChangeValidation {
47
+ /** 유효 여부 */
48
+ valid: boolean;
49
+ /** 보호 위반 목록 */
50
+ violations: ProtectionViolation[];
51
+ }
52
+
53
+ // ============================================
54
+ // 보호 필드 추출
55
+ // ============================================
56
+
57
+ /**
58
+ * Zod 스키마에서 보호된 필드 목록 추출
59
+ *
60
+ * @param schema Zod 스키마
61
+ * @param basePath 기본 경로 (재귀용)
62
+ * @returns 보호된 필드 정보 목록
63
+ *
64
+ * @example
65
+ * ```typescript
66
+ * const schema = z.object({
67
+ * apiKey: sensitiveToken(),
68
+ * config: z.object({
69
+ * secret: protectedField("Security"),
70
+ * }),
71
+ * });
72
+ *
73
+ * const fields = extractProtectedFields(schema);
74
+ * // [
75
+ * // { path: "apiKey", reason: "Sensitive token...", ... },
76
+ * // { path: "config.secret", reason: "Security", ... },
77
+ * // ]
78
+ * ```
79
+ */
80
+ export function extractProtectedFields(
81
+ schema: z.ZodType,
82
+ basePath = ""
83
+ ): ProtectedFieldInfo[] {
84
+ const fields: ProtectedFieldInfo[] = [];
85
+
86
+ // ZodObject 처리
87
+ if (schema instanceof z.ZodObject) {
88
+ const shape = schema.shape as Record<string, z.ZodType>;
89
+
90
+ for (const [key, value] of Object.entries(shape)) {
91
+ const currentPath = basePath ? `${basePath}.${key}` : key;
92
+
93
+ // 보호된 필드 확인
94
+ if (isProtectedField(value)) {
95
+ const meta = getMetadata(value, PROTECTED_FIELD) as ProtectedFieldMetadata | undefined;
96
+ fields.push({
97
+ path: currentPath,
98
+ reason: meta?.reason ?? "Protected field",
99
+ allowedModifiers: meta?.allowedModifiers ?? ["human"],
100
+ isSensitive: isSensitiveField(value),
101
+ });
102
+ }
103
+ // 민감 필드도 보호 대상
104
+ else if (isSensitiveField(value)) {
105
+ const meta = getMetadata(value, SENSITIVE_FIELD) as SensitiveFieldMetadata | undefined;
106
+ fields.push({
107
+ path: currentPath,
108
+ reason: "Sensitive field - redacted in logs",
109
+ allowedModifiers: ["human"],
110
+ isSensitive: true,
111
+ });
112
+ }
113
+
114
+ // 중첩 객체 재귀 탐색
115
+ if (value instanceof z.ZodObject) {
116
+ const nested = extractProtectedFields(value, currentPath);
117
+ fields.push(...nested);
118
+ }
119
+ // Optional 처리
120
+ else if (value instanceof z.ZodOptional) {
121
+ const inner = value.unwrap();
122
+ if (inner instanceof z.ZodObject) {
123
+ const nested = extractProtectedFields(inner, currentPath);
124
+ fields.push(...nested);
125
+ }
126
+ }
127
+ // Nullable 처리
128
+ else if (value instanceof z.ZodNullable) {
129
+ const inner = value.unwrap();
130
+ if (inner instanceof z.ZodObject) {
131
+ const nested = extractProtectedFields(inner, currentPath);
132
+ fields.push(...nested);
133
+ }
134
+ }
135
+ }
136
+ }
137
+
138
+ return fields;
139
+ }
140
+
141
+ /**
142
+ * Contract 스키마 전체에서 보호 필드 추출
143
+ */
144
+ export function extractContractProtectedFields(
145
+ contract: { request?: unknown; response?: unknown }
146
+ ): {
147
+ request: ProtectedFieldInfo[];
148
+ response: ProtectedFieldInfo[];
149
+ } {
150
+ const request: ProtectedFieldInfo[] = [];
151
+ const response: ProtectedFieldInfo[] = [];
152
+
153
+ // Request 스키마 탐색
154
+ if (contract.request && typeof contract.request === "object") {
155
+ for (const [method, schema] of Object.entries(contract.request)) {
156
+ if (schema && typeof schema === "object") {
157
+ const methodSchema = schema as Record<string, z.ZodType>;
158
+
159
+ // body, query, params, headers
160
+ for (const [part, partSchema] of Object.entries(methodSchema)) {
161
+ if (partSchema instanceof z.ZodType) {
162
+ const fields = extractProtectedFields(partSchema, `${method}.${part}`);
163
+ request.push(...fields);
164
+ }
165
+ }
166
+ }
167
+ }
168
+ }
169
+
170
+ // Response 스키마 탐색
171
+ if (contract.response && typeof contract.response === "object") {
172
+ for (const [status, schema] of Object.entries(contract.response)) {
173
+ if (schema instanceof z.ZodType) {
174
+ const fields = extractProtectedFields(schema, `${status}`);
175
+ response.push(...fields);
176
+ }
177
+ }
178
+ }
179
+
180
+ return { request, response };
181
+ }
182
+
183
+ // ============================================
184
+ // 변경 검증
185
+ // ============================================
186
+
187
+ /**
188
+ * 객체에서 경로로 값 가져오기
189
+ */
190
+ function getValueByPath(obj: unknown, path: string): unknown {
191
+ const parts = path.split(".");
192
+ let current: unknown = obj;
193
+
194
+ for (const part of parts) {
195
+ if (current === null || current === undefined) {
196
+ return undefined;
197
+ }
198
+ if (typeof current !== "object") {
199
+ return undefined;
200
+ }
201
+ current = (current as Record<string, unknown>)[part];
202
+ }
203
+
204
+ return current;
205
+ }
206
+
207
+ /**
208
+ * Contract 변경 시 보호 필드 검증
209
+ *
210
+ * @param oldSchema 이전 스키마
211
+ * @param newSchema 새 스키마
212
+ * @param modifier 수정자 ("human" | "ai")
213
+ * @returns 검증 결과
214
+ *
215
+ * @example
216
+ * ```typescript
217
+ * const validation = validateContractChanges(
218
+ * oldContract.request,
219
+ * newContract.request,
220
+ * "ai"
221
+ * );
222
+ *
223
+ * if (!validation.valid) {
224
+ * console.error("AI가 보호된 필드를 수정하려고 합니다:", validation.violations);
225
+ * }
226
+ * ```
227
+ */
228
+ export function validateContractChanges(
229
+ oldSchema: z.ZodType,
230
+ newSchema: z.ZodType,
231
+ modifier: "human" | "ai"
232
+ ): ContractChangeValidation {
233
+ const violations: ProtectionViolation[] = [];
234
+
235
+ // 이전 스키마에서 보호 필드 추출
236
+ const protectedFields = extractProtectedFields(oldSchema);
237
+
238
+ for (const field of protectedFields) {
239
+ // 수정 권한 확인
240
+ if (!field.allowedModifiers.includes(modifier)) {
241
+ // 스키마 구조 변경 감지 (간단한 비교)
242
+ const oldValue = getSchemaDefinition(oldSchema, field.path);
243
+ const newValue = getSchemaDefinition(newSchema, field.path);
244
+
245
+ // 구조가 변경되었는지 확인
246
+ if (hasSchemaChanged(oldValue, newValue)) {
247
+ violations.push({
248
+ field: field.path,
249
+ reason: field.reason,
250
+ message: `${modifier}는 보호된 필드 '${field.path}'를 수정할 수 없습니다`,
251
+ modifier,
252
+ });
253
+ }
254
+ }
255
+ }
256
+
257
+ return {
258
+ valid: violations.length === 0,
259
+ violations,
260
+ };
261
+ }
262
+
263
+ /**
264
+ * 스키마에서 경로로 정의 가져오기
265
+ */
266
+ function getSchemaDefinition(schema: z.ZodType, path: string): z.ZodType | undefined {
267
+ const parts = path.split(".");
268
+ let current: z.ZodType | undefined = schema;
269
+
270
+ for (const part of parts) {
271
+ if (!current) return undefined;
272
+
273
+ if (current instanceof z.ZodObject) {
274
+ current = current.shape[part] as z.ZodType | undefined;
275
+ } else if (current instanceof z.ZodOptional) {
276
+ current = current.unwrap();
277
+ if (current instanceof z.ZodObject) {
278
+ current = current.shape[part] as z.ZodType | undefined;
279
+ }
280
+ } else {
281
+ return undefined;
282
+ }
283
+ }
284
+
285
+ return current;
286
+ }
287
+
288
+ /**
289
+ * 스키마가 변경되었는지 확인 (간단한 비교)
290
+ */
291
+ function hasSchemaChanged(
292
+ oldSchema: z.ZodType | undefined,
293
+ newSchema: z.ZodType | undefined
294
+ ): boolean {
295
+ // 둘 다 없으면 변경 없음
296
+ if (!oldSchema && !newSchema) return false;
297
+
298
+ // 하나만 있으면 변경됨
299
+ if (!oldSchema || !newSchema) return true;
300
+
301
+ // 타입이 다르면 변경됨
302
+ const oldTypeName = (oldSchema._def as { typeName?: string }).typeName;
303
+ const newTypeName = (newSchema._def as { typeName?: string }).typeName;
304
+ if (oldTypeName !== newTypeName) {
305
+ return true;
306
+ }
307
+
308
+ // ZodObject의 경우 shape 키 비교
309
+ if (oldSchema instanceof z.ZodObject && newSchema instanceof z.ZodObject) {
310
+ const oldKeys = Object.keys(oldSchema.shape);
311
+ const newKeys = Object.keys(newSchema.shape);
312
+
313
+ if (oldKeys.length !== newKeys.length) return true;
314
+
315
+ for (const key of oldKeys) {
316
+ if (!newKeys.includes(key)) return true;
317
+ }
318
+ }
319
+
320
+ return false;
321
+ }
322
+
323
+ // ============================================
324
+ // 포맷팅
325
+ // ============================================
326
+
327
+ /**
328
+ * 보호 필드 목록을 문자열로 포맷
329
+ */
330
+ export function formatProtectedFields(fields: ProtectedFieldInfo[]): string {
331
+ if (fields.length === 0) {
332
+ return "보호된 필드 없음";
333
+ }
334
+
335
+ const lines: string[] = ["보호된 필드:"];
336
+
337
+ for (const field of fields) {
338
+ const sensitive = field.isSensitive ? " 🔐" : "";
339
+ lines.push(` - ${field.path}${sensitive}`);
340
+ lines.push(` 이유: ${field.reason}`);
341
+ lines.push(` 수정 가능: ${field.allowedModifiers.join(", ")}`);
342
+ }
343
+
344
+ return lines.join("\n");
345
+ }
346
+
347
+ /**
348
+ * 보호 위반 목록을 문자열로 포맷
349
+ */
350
+ export function formatProtectionViolations(violations: ProtectionViolation[]): string {
351
+ if (violations.length === 0) {
352
+ return "위반 없음";
353
+ }
354
+
355
+ const lines: string[] = ["🛑 보호 필드 위반:"];
356
+
357
+ for (const v of violations) {
358
+ lines.push(` - ${v.field}`);
359
+ lines.push(` ${v.message}`);
360
+ lines.push(` 이유: ${v.reason}`);
361
+ }
362
+
363
+ return lines.join("\n");
364
+ }
@@ -0,0 +1,265 @@
1
+ /**
2
+ * 도메인별 에러 클래스
3
+ *
4
+ * ManduError 인터페이스 기반의 실제 Error 클래스들.
5
+ * try-catch에서 instanceof 체크 가능.
6
+ */
7
+
8
+ import { ErrorCode, ERROR_MESSAGES, ERROR_SUMMARIES } from "./types";
9
+ import type { ManduError, RouteContext, ErrorType } from "./types";
10
+
11
+ /**
12
+ * Mandu 에러 베이스 클래스
13
+ */
14
+ export abstract class ManduBaseError extends Error implements ManduError {
15
+ abstract readonly errorType: ErrorType;
16
+ readonly code: ErrorCode | string;
17
+ readonly httpStatus?: number;
18
+ readonly summary: string;
19
+ readonly fix: { file: string; suggestion: string; line?: number };
20
+ readonly route?: RouteContext;
21
+ readonly timestamp: string;
22
+
23
+ constructor(
24
+ code: ErrorCode | string,
25
+ message: string,
26
+ fix: { file: string; suggestion: string; line?: number },
27
+ options?: {
28
+ httpStatus?: number;
29
+ route?: RouteContext;
30
+ cause?: unknown;
31
+ }
32
+ ) {
33
+ super(message, { cause: options?.cause });
34
+ this.name = this.constructor.name;
35
+ this.code = code;
36
+ this.httpStatus = options?.httpStatus;
37
+ this.summary =
38
+ typeof code === "string" && code in ERROR_SUMMARIES
39
+ ? ERROR_SUMMARIES[code as ErrorCode]
40
+ : message;
41
+ this.fix = fix;
42
+ this.route = options?.route;
43
+ this.timestamp = new Date().toISOString();
44
+ }
45
+
46
+ /**
47
+ * ManduError 인터페이스로 변환
48
+ */
49
+ toManduError(): ManduError {
50
+ return {
51
+ errorType: this.errorType,
52
+ code: this.code,
53
+ httpStatus: this.httpStatus,
54
+ message: this.message,
55
+ summary: this.summary,
56
+ fix: this.fix,
57
+ route: this.route,
58
+ timestamp: this.timestamp,
59
+ };
60
+ }
61
+ }
62
+
63
+ // ============================================================
64
+ // 파일 시스템 에러
65
+ // ============================================================
66
+
67
+ /**
68
+ * 파일 읽기/쓰기 에러
69
+ */
70
+ export class FileError extends ManduBaseError {
71
+ readonly errorType = "LOGIC_ERROR" as const;
72
+ readonly filePath: string;
73
+ readonly operation: "read" | "write" | "access" | "stat";
74
+
75
+ constructor(
76
+ filePath: string,
77
+ operation: "read" | "write" | "access" | "stat",
78
+ cause?: unknown
79
+ ) {
80
+ const message = `파일 ${operation} 실패: ${filePath}`;
81
+ super(
82
+ ErrorCode.SLOT_IMPORT_ERROR,
83
+ message,
84
+ {
85
+ file: filePath,
86
+ suggestion: `파일이 존재하고 읽기 권한이 있는지 확인하세요`,
87
+ },
88
+ { cause }
89
+ );
90
+ this.filePath = filePath;
91
+ this.operation = operation;
92
+ }
93
+ }
94
+
95
+ /**
96
+ * 디렉토리 읽기 에러
97
+ */
98
+ export class DirectoryError extends ManduBaseError {
99
+ readonly errorType = "LOGIC_ERROR" as const;
100
+ readonly dirPath: string;
101
+
102
+ constructor(dirPath: string, cause?: unknown) {
103
+ super(
104
+ ErrorCode.SLOT_NOT_FOUND,
105
+ `디렉토리 읽기 실패: ${dirPath}`,
106
+ {
107
+ file: dirPath,
108
+ suggestion: `디렉토리가 존재하고 접근 가능한지 확인하세요`,
109
+ },
110
+ { cause }
111
+ );
112
+ this.dirPath = dirPath;
113
+ }
114
+ }
115
+
116
+ // ============================================================
117
+ // Guard 에러
118
+ // ============================================================
119
+
120
+ /**
121
+ * Guard 아키텍처 검사 에러
122
+ */
123
+ export class GuardError extends ManduBaseError {
124
+ readonly errorType = "LOGIC_ERROR" as const;
125
+ readonly ruleId: string;
126
+
127
+ constructor(
128
+ ruleId: string,
129
+ message: string,
130
+ file: string,
131
+ options?: {
132
+ line?: number;
133
+ suggestion?: string;
134
+ cause?: unknown;
135
+ }
136
+ ) {
137
+ super(
138
+ ErrorCode.SLOT_VALIDATION_ERROR,
139
+ message,
140
+ {
141
+ file,
142
+ suggestion: options?.suggestion || "아키텍처 규칙을 확인하세요",
143
+ line: options?.line,
144
+ },
145
+ { cause: options?.cause }
146
+ );
147
+ this.ruleId = ruleId;
148
+ }
149
+ }
150
+
151
+ // ============================================================
152
+ // Router 에러
153
+ // ============================================================
154
+
155
+ /**
156
+ * 라우터 에러
157
+ */
158
+ export class RouterError extends ManduBaseError {
159
+ readonly errorType = "FRAMEWORK_BUG" as const;
160
+
161
+ constructor(
162
+ message: string,
163
+ file: string,
164
+ options?: {
165
+ route?: RouteContext;
166
+ cause?: unknown;
167
+ }
168
+ ) {
169
+ super(
170
+ ErrorCode.FRAMEWORK_ROUTER_ERROR,
171
+ message,
172
+ {
173
+ file,
174
+ suggestion: "라우트 설정을 확인하세요",
175
+ },
176
+ { httpStatus: 500, ...options }
177
+ );
178
+ }
179
+ }
180
+
181
+ // ============================================================
182
+ // SSR 에러
183
+ // ============================================================
184
+
185
+ /**
186
+ * SSR 렌더링 에러
187
+ */
188
+ export class SSRError extends ManduBaseError {
189
+ readonly errorType = "FRAMEWORK_BUG" as const;
190
+
191
+ constructor(
192
+ message: string,
193
+ route: RouteContext,
194
+ cause?: unknown
195
+ ) {
196
+ super(
197
+ ErrorCode.FRAMEWORK_SSR_ERROR,
198
+ message,
199
+ {
200
+ file: `app/${route.id}/page.tsx`,
201
+ suggestion: "페이지 컴포넌트에서 렌더링 오류가 발생했습니다",
202
+ },
203
+ { httpStatus: 500, route, cause }
204
+ );
205
+ }
206
+ }
207
+
208
+ // ============================================================
209
+ // Contract 에러
210
+ // ============================================================
211
+
212
+ /**
213
+ * API 계약 위반 에러
214
+ */
215
+ export class ContractError extends ManduBaseError {
216
+ readonly errorType = "LOGIC_ERROR" as const;
217
+
218
+ constructor(
219
+ message: string,
220
+ contractFile: string,
221
+ options?: {
222
+ route?: RouteContext;
223
+ cause?: unknown;
224
+ }
225
+ ) {
226
+ super(
227
+ ErrorCode.SLOT_VALIDATION_ERROR,
228
+ message,
229
+ {
230
+ file: contractFile,
231
+ suggestion: "API 계약과 실제 구현이 일치하는지 확인하세요",
232
+ },
233
+ { httpStatus: 400, ...options }
234
+ );
235
+ }
236
+ }
237
+
238
+ // ============================================================
239
+ // Security 에러
240
+ // ============================================================
241
+
242
+ /**
243
+ * 보안 관련 에러
244
+ */
245
+ export class SecurityError extends ManduBaseError {
246
+ readonly errorType = "LOGIC_ERROR" as const;
247
+ readonly securityType: "path_traversal" | "injection" | "unauthorized" | "import_violation";
248
+
249
+ constructor(
250
+ securityType: "path_traversal" | "injection" | "unauthorized" | "import_violation",
251
+ message: string,
252
+ file?: string
253
+ ) {
254
+ super(
255
+ ErrorCode.SLOT_HANDLER_ERROR,
256
+ message,
257
+ {
258
+ file: file || "unknown",
259
+ suggestion: "보안 정책을 위반하는 요청입니다",
260
+ },
261
+ { httpStatus: 403 }
262
+ );
263
+ this.securityType = securityType;
264
+ }
265
+ }
@@ -2,6 +2,18 @@
2
2
  export type { ManduError, RouteContext, FixTarget, DebugInfo, ErrorType } from "./types";
3
3
  export { ErrorCode, ERROR_MESSAGES, ERROR_SUMMARIES } from "./types";
4
4
 
5
+ // Domain Error Classes
6
+ export {
7
+ ManduBaseError,
8
+ FileError,
9
+ DirectoryError,
10
+ GuardError,
11
+ RouterError,
12
+ SSRError,
13
+ ContractError,
14
+ SecurityError,
15
+ } from "./domains";
16
+
5
17
  // Stack Analyzer
6
18
  export { StackTraceAnalyzer, type StackFrame } from "./stack-analyzer";
7
19
 
@@ -14,16 +26,16 @@ export {
14
26
  } from "./classifier";
15
27
 
16
28
  // Formatter
17
- export {
18
- formatErrorResponse,
19
- formatErrorForConsole,
20
- createNotFoundResponse,
21
- createHandlerNotFoundResponse,
22
- createPageLoadErrorResponse,
23
- createSSRErrorResponse,
24
- type FormatOptions,
25
- } from "./formatter";
26
-
27
- // Result helpers
28
- export type { Result } from "./result";
29
- export { ok, err, statusFromError, errorToResponse } from "./result";
29
+ export {
30
+ formatErrorResponse,
31
+ formatErrorForConsole,
32
+ createNotFoundResponse,
33
+ createHandlerNotFoundResponse,
34
+ createPageLoadErrorResponse,
35
+ createSSRErrorResponse,
36
+ type FormatOptions,
37
+ } from "./formatter";
38
+
39
+ // Result helpers
40
+ export type { Result } from "./result";
41
+ export { ok, err, statusFromError, errorToResponse } from "./result";