@mandujs/core 0.9.39 → 0.9.40

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 (49) hide show
  1. package/README.ko.md +27 -0
  2. package/README.md +21 -5
  3. package/package.json +1 -1
  4. package/src/config/index.ts +1 -0
  5. package/src/config/mandu.ts +60 -0
  6. package/src/contract/client-safe.test.ts +42 -0
  7. package/src/contract/client-safe.ts +114 -0
  8. package/src/contract/client.ts +12 -11
  9. package/src/contract/handler.ts +10 -11
  10. package/src/contract/index.ts +25 -16
  11. package/src/contract/registry.test.ts +206 -0
  12. package/src/contract/registry.ts +568 -0
  13. package/src/contract/schema.ts +48 -12
  14. package/src/contract/types.ts +58 -35
  15. package/src/contract/validator.ts +32 -17
  16. package/src/filling/context.ts +103 -0
  17. package/src/generator/templates.ts +70 -17
  18. package/src/guard/analyzer.ts +9 -4
  19. package/src/guard/check.ts +66 -30
  20. package/src/guard/contract-guard.ts +9 -9
  21. package/src/guard/file-type.test.ts +24 -0
  22. package/src/guard/presets/index.ts +193 -60
  23. package/src/guard/rules.ts +12 -6
  24. package/src/guard/statistics.ts +6 -0
  25. package/src/guard/suggestions.ts +9 -2
  26. package/src/guard/types.ts +11 -1
  27. package/src/guard/validator.ts +160 -9
  28. package/src/guard/watcher.ts +2 -0
  29. package/src/index.ts +8 -1
  30. package/src/runtime/index.ts +1 -0
  31. package/src/runtime/streaming-ssr.ts +123 -2
  32. package/src/seo/index.ts +214 -0
  33. package/src/seo/integration/ssr.ts +307 -0
  34. package/src/seo/render/basic.ts +427 -0
  35. package/src/seo/render/index.ts +143 -0
  36. package/src/seo/render/jsonld.ts +539 -0
  37. package/src/seo/render/opengraph.ts +191 -0
  38. package/src/seo/render/robots.ts +116 -0
  39. package/src/seo/render/sitemap.ts +137 -0
  40. package/src/seo/render/twitter.ts +126 -0
  41. package/src/seo/resolve/index.ts +353 -0
  42. package/src/seo/resolve/opengraph.ts +143 -0
  43. package/src/seo/resolve/robots.ts +73 -0
  44. package/src/seo/resolve/title.ts +94 -0
  45. package/src/seo/resolve/twitter.ts +73 -0
  46. package/src/seo/resolve/url.ts +97 -0
  47. package/src/seo/routes/index.ts +290 -0
  48. package/src/seo/types.ts +575 -0
  49. package/src/slot/validator.ts +39 -16
@@ -11,10 +11,22 @@ import type {
11
11
  MethodRequestSchema,
12
12
  } from "./schema";
13
13
 
14
- /**
15
- * Extract inferred type from Zod schema
16
- */
17
- type InferZod<T> = T extends z.ZodTypeAny ? z.infer<T> : never;
14
+ /**
15
+ * Extract inferred type from Zod schema
16
+ */
17
+ type InferZod<T> = T extends z.ZodTypeAny ? z.infer<T> : never;
18
+
19
+ /**
20
+ * Extract inferred type from response schema (supports ResponseSchemaWithExamples)
21
+ */
22
+ export type InferResponseSchema<T> =
23
+ T extends { schema: infer S }
24
+ ? S extends z.ZodTypeAny
25
+ ? z.infer<S>
26
+ : never
27
+ : T extends z.ZodTypeAny
28
+ ? z.infer<T>
29
+ : never;
18
30
 
19
31
  /**
20
32
  * Infer request schema types for a single method
@@ -38,9 +50,9 @@ type InferContractRequest<T extends ContractRequestSchema> = {
38
50
  /**
39
51
  * Infer all response schemas
40
52
  */
41
- type InferContractResponse<T extends ContractResponseSchema> = {
42
- [K in keyof T]: T[K] extends z.ZodTypeAny ? z.infer<T[K]> : never;
43
- };
53
+ type InferContractResponse<T extends ContractResponseSchema> = {
54
+ [K in keyof T]: InferResponseSchema<T[K]>;
55
+ };
44
56
 
45
57
  /**
46
58
  * Infer full contract types
@@ -103,22 +115,34 @@ export type InferBody<
103
115
  /**
104
116
  * Extract params type for a specific method
105
117
  */
106
- export type InferParams<
107
- T extends ContractSchema,
108
- M extends keyof T["request"]
109
- > = T["request"][M] extends MethodRequestSchema
110
- ? T["request"][M]["params"] extends z.ZodTypeAny
111
- ? z.infer<T["request"][M]["params"]>
112
- : undefined
113
- : undefined;
118
+ export type InferParams<
119
+ T extends ContractSchema,
120
+ M extends keyof T["request"]
121
+ > = T["request"][M] extends MethodRequestSchema
122
+ ? T["request"][M]["params"] extends z.ZodTypeAny
123
+ ? z.infer<T["request"][M]["params"]>
124
+ : undefined
125
+ : undefined;
126
+
127
+ /**
128
+ * Extract headers type for a specific method
129
+ */
130
+ export type InferHeaders<
131
+ T extends ContractSchema,
132
+ M extends keyof T["request"]
133
+ > = T["request"][M] extends MethodRequestSchema
134
+ ? T["request"][M]["headers"] extends z.ZodTypeAny
135
+ ? z.infer<T["request"][M]["headers"]>
136
+ : undefined
137
+ : undefined;
114
138
 
115
139
  /**
116
140
  * Extract response type for a specific status code
117
141
  */
118
- export type InferResponse<
119
- T extends ContractSchema,
120
- S extends keyof T["response"]
121
- > = T["response"][S] extends z.ZodTypeAny ? z.infer<T["response"][S]> : never;
142
+ export type InferResponse<
143
+ T extends ContractSchema,
144
+ S extends keyof T["response"]
145
+ > = InferResponseSchema<T["response"][S]>;
122
146
 
123
147
  /**
124
148
  * Helper type to get all defined methods in a contract
@@ -159,21 +183,20 @@ export type RequiredFields<
159
183
  /**
160
184
  * Get the success response type (200 or 201)
161
185
  */
162
- export type SuccessResponse<T extends ContractSchema> = T["response"][200] extends z.ZodTypeAny
163
- ? z.infer<T["response"][200]>
164
- : T["response"][201] extends z.ZodTypeAny
165
- ? z.infer<T["response"][201]>
166
- : never;
186
+ export type SuccessResponse<T extends ContractSchema> =
187
+ InferResponseSchema<T["response"][200]> extends never
188
+ ? InferResponseSchema<T["response"][201]>
189
+ : InferResponseSchema<T["response"][200]>;
167
190
 
168
191
  /**
169
192
  * Get the error response type (400, 404, 500, etc.)
170
193
  */
171
- export type ErrorResponse<T extends ContractSchema> =
172
- | (T["response"][400] extends z.ZodTypeAny ? z.infer<T["response"][400]> : never)
173
- | (T["response"][401] extends z.ZodTypeAny ? z.infer<T["response"][401]> : never)
174
- | (T["response"][403] extends z.ZodTypeAny ? z.infer<T["response"][403]> : never)
175
- | (T["response"][404] extends z.ZodTypeAny ? z.infer<T["response"][404]> : never)
176
- | (T["response"][500] extends z.ZodTypeAny ? z.infer<T["response"][500]> : never);
194
+ export type ErrorResponse<T extends ContractSchema> =
195
+ | InferResponseSchema<T["response"][400]>
196
+ | InferResponseSchema<T["response"][401]>
197
+ | InferResponseSchema<T["response"][403]>
198
+ | InferResponseSchema<T["response"][404]>
199
+ | InferResponseSchema<T["response"][500]>;
177
200
 
178
201
  /**
179
202
  * Utility type for strict contract enforcement
@@ -210,8 +233,8 @@ export type ContractFetchOptions<
210
233
  /**
211
234
  * Response type union for a contract
212
235
  */
213
- export type ContractResponseUnion<T extends ContractSchema> = {
214
- [K in keyof T["response"]]: T["response"][K] extends z.ZodTypeAny
215
- ? { status: K; data: z.infer<T["response"][K]> }
216
- : never;
217
- }[keyof T["response"]];
236
+ export type ContractResponseUnion<T extends ContractSchema> = {
237
+ [K in keyof T["response"]]: InferResponseSchema<T["response"][K]> extends never
238
+ ? never
239
+ : { status: K; data: InferResponseSchema<T["response"][K]> };
240
+ }[keyof T["response"]];
@@ -13,21 +13,32 @@
13
13
  */
14
14
 
15
15
  import type { z } from "zod";
16
- import type {
17
- ContractSchema,
18
- ContractValidationResult,
19
- ContractValidationError,
20
- ContractValidationIssue,
21
- MethodRequestSchema,
22
- ContractNormalizeMode,
23
- } from "./schema";
16
+ import type {
17
+ ContractSchema,
18
+ ContractValidationResult,
19
+ ContractValidationError,
20
+ ContractValidationIssue,
21
+ MethodRequestSchema,
22
+ ContractNormalizeMode,
23
+ ResponseSchemaWithExamples,
24
+ } from "./schema";
24
25
  import {
25
26
  type NormalizeMode,
26
27
  type NormalizeOptions,
27
28
  normalizeSchema,
28
29
  createCoerceSchema,
29
30
  } from "./normalize";
30
- import { ZodObject } from "zod";
31
+ import { ZodObject } from "zod";
32
+
33
+ function isResponseSchemaWithExamples(
34
+ schema: z.ZodTypeAny | ResponseSchemaWithExamples | undefined
35
+ ): schema is ResponseSchemaWithExamples {
36
+ return (
37
+ schema !== undefined &&
38
+ typeof schema === "object" &&
39
+ "schema" in schema
40
+ );
41
+ }
31
42
 
32
43
  /**
33
44
  * Validator 옵션
@@ -442,14 +453,18 @@ export class ContractValidator {
442
453
  * @param responseBody - The response body (already parsed)
443
454
  * @param statusCode - HTTP status code
444
455
  */
445
- validateResponse(responseBody: unknown, statusCode: number): ContractValidationResult {
446
- const responseSchema = this.contract.response[statusCode];
447
- if (!responseSchema) {
448
- // No schema defined for this status code, pass through
449
- return { success: true };
450
- }
451
-
452
- const result = responseSchema.safeParse(responseBody);
456
+ validateResponse(responseBody: unknown, statusCode: number): ContractValidationResult {
457
+ const responseSchemaOrWithExamples = this.contract.response[statusCode];
458
+ if (!responseSchemaOrWithExamples) {
459
+ // No schema defined for this status code, pass through
460
+ return { success: true };
461
+ }
462
+
463
+ const responseSchema = isResponseSchemaWithExamples(responseSchemaOrWithExamples)
464
+ ? responseSchemaOrWithExamples.schema
465
+ : responseSchemaOrWithExamples;
466
+
467
+ const result = responseSchema.safeParse(responseBody);
453
468
  if (!result.success) {
454
469
  const errors: ContractValidationError[] = [
455
470
  {
@@ -4,6 +4,19 @@
4
4
  */
5
5
 
6
6
  import type { ZodSchema } from "zod";
7
+ import type { ContractSchema, ContractMethod } from "../contract/schema";
8
+ import type { InferBody, InferHeaders, InferParams, InferQuery, InferResponse } from "../contract/types";
9
+ import { ContractValidator, type ContractValidatorOptions } from "../contract/validator";
10
+
11
+ type ContractInput<
12
+ TContract extends ContractSchema,
13
+ TMethod extends ContractMethod,
14
+ > = {
15
+ query: InferQuery<TContract, TMethod>;
16
+ body: InferBody<TContract, TMethod>;
17
+ params: InferParams<TContract, TMethod>;
18
+ headers: InferHeaders<TContract, TMethod>;
19
+ };
7
20
 
8
21
  // ========== Cookie Types ==========
9
22
 
@@ -316,6 +329,34 @@ export class ManduContext {
316
329
  return data as T;
317
330
  }
318
331
 
332
+ /**
333
+ * Parse and validate request input via Contract
334
+ * @example
335
+ * const input = await ctx.input(userContract, "POST", { id: "123" })
336
+ */
337
+ async input<
338
+ TContract extends ContractSchema,
339
+ TMethod extends ContractMethod,
340
+ >(
341
+ contract: TContract,
342
+ method: TMethod,
343
+ pathParams: Record<string, string> = {},
344
+ options: ContractValidatorOptions = {}
345
+ ): Promise<ContractInput<TContract, TMethod>> {
346
+ const validator = new ContractValidator(contract, options);
347
+ const result = await validator.validateAndNormalizeRequest(
348
+ this.request,
349
+ method,
350
+ pathParams
351
+ );
352
+
353
+ if (!result.success) {
354
+ throw new ValidationError(result.errors ?? []);
355
+ }
356
+
357
+ return (result.data ?? {}) as ContractInput<TContract, TMethod>;
358
+ }
359
+
319
360
  // ============================================
320
361
  // 🥟 Response 보내기
321
362
  // ============================================
@@ -376,6 +417,68 @@ export class ManduContext {
376
417
  return this.withCookies(response);
377
418
  }
378
419
 
420
+ /**
421
+ * Validate and send response via Contract
422
+ * @example
423
+ * return ctx.output(userContract, 200, { data: users })
424
+ */
425
+ output<
426
+ TContract extends ContractSchema,
427
+ TStatus extends keyof TContract["response"],
428
+ >(
429
+ contract: TContract,
430
+ status: TStatus,
431
+ data: InferResponse<TContract, TStatus>,
432
+ options: ContractValidatorOptions = {}
433
+ ): Response {
434
+ const validator = new ContractValidator(contract, options);
435
+ const result = validator.validateResponse(data, Number(status));
436
+
437
+ if (!result.success) {
438
+ if (options.mode === "strict") {
439
+ const errorResponse = Response.json(
440
+ {
441
+ errorType: "CONTRACT_VIOLATION",
442
+ code: "MANDU_C001",
443
+ message: "Response does not match contract schema",
444
+ summary: "응답이 Contract 스키마와 일치하지 않습니다",
445
+ statusCode: Number(status),
446
+ violations: result.errors,
447
+ timestamp: new Date().toISOString(),
448
+ },
449
+ { status: 500 }
450
+ );
451
+ return this.withCookies(errorResponse);
452
+ }
453
+
454
+ console.warn(
455
+ "\x1b[33m[Mandu] Contract violation in response:\x1b[0m",
456
+ result.errors
457
+ );
458
+ }
459
+
460
+ const payload = result.success ? result.data : data;
461
+ return this.json(payload as InferResponse<TContract, TStatus>, Number(status));
462
+ }
463
+
464
+ /** 200 OK with Contract validation */
465
+ okContract<TContract extends ContractSchema>(
466
+ contract: TContract,
467
+ data: InferResponse<TContract, 200>,
468
+ options: ContractValidatorOptions = {}
469
+ ): Response {
470
+ return this.output(contract, 200 as keyof TContract["response"], data, options);
471
+ }
472
+
473
+ /** 201 Created with Contract validation */
474
+ createdContract<TContract extends ContractSchema>(
475
+ contract: TContract,
476
+ data: InferResponse<TContract, 201>,
477
+ options: ContractValidatorOptions = {}
478
+ ): Response {
479
+ return this.output(contract, 201 as keyof TContract["response"], data, options);
480
+ }
481
+
379
482
  /** Custom text response */
380
483
  text(data: string, status: number = 200): Response {
381
484
  const response = new Response(data, {
@@ -110,10 +110,14 @@ export default async function handler(
110
110
  `;
111
111
  }
112
112
 
113
- export function generateSlotLogic(route: RouteSpec): string {
114
- return `// 🥟 Mandu Filling - ${route.id}
115
- // Pattern: ${route.pattern}
116
- // 이 파일에서 비즈니스 로직을 구현하세요.
113
+ export function generateSlotLogic(route: RouteSpec): string {
114
+ if (route.contractModule) {
115
+ return generateSlotLogicWithContract(route);
116
+ }
117
+
118
+ return `// 🥟 Mandu Filling - ${route.id}
119
+ // Pattern: ${route.pattern}
120
+ // 이 파일에서 비즈니스 로직을 구현하세요.
117
121
 
118
122
  import { Mandu } from "@mandujs/core";
119
123
 
@@ -145,17 +149,62 @@ export default Mandu.filling()
145
149
  // 💡 Context (ctx) API:
146
150
  // ctx.query - Query parameters { name: 'value' }
147
151
  // ctx.params - Path parameters { id: '123' }
148
- // ctx.body() - Request body (자동 파싱)
149
- // ctx.body(zodSchema) - Body with validation
150
- // ctx.headers - Request headers
151
- // ctx.ok(data) - 200 OK
152
- // ctx.created(data) - 201 Created
153
- // ctx.error(msg) - 400 Bad Request
154
- // ctx.notFound(msg) - 404 Not Found
155
- // ctx.set(key, value) - Guard에서 Handler로 데이터 전달
156
- // ctx.get(key) - Guard에서 설정한 데이터 읽기
157
- `;
158
- }
152
+ // ctx.body() - Request body (자동 파싱)
153
+ // ctx.body(zodSchema) - Body with validation
154
+ // ctx.headers - Request headers
155
+ // ctx.ok(data) - 200 OK
156
+ // ctx.created(data) - 201 Created
157
+ // ctx.error(msg) - 400 Bad Request
158
+ // ctx.notFound(msg) - 404 Not Found
159
+ // ctx.set(key, value) - Guard에서 Handler로 데이터 전달
160
+ // ctx.get(key) - Guard에서 설정한 데이터 읽기
161
+ `;
162
+ }
163
+
164
+ export function generateSlotLogicWithContract(route: RouteSpec): string {
165
+ const contractImportPath = computeSlotImportPath(
166
+ route.contractModule!,
167
+ pathDirname(route.slotModule ?? "spec/slots")
168
+ );
169
+
170
+ return `// 🥟 Mandu Filling - ${route.id}
171
+ // Pattern: ${route.pattern}
172
+ // Contract Module: ${route.contractModule}
173
+ // 이 파일에서 비즈니스 로직을 구현하세요.
174
+
175
+ import { Mandu } from "@mandujs/core";
176
+ import contract from "${contractImportPath}";
177
+
178
+ export default Mandu.filling()
179
+ // 📋 GET ${route.pattern}
180
+ .get(async (ctx) => {
181
+ const input = await ctx.input(contract, "GET", ctx.params);
182
+ // TODO: 계약의 응답 코드에 맞게 status를 조정하세요
183
+ return ctx.output(contract, 200, {
184
+ message: "Hello from ${route.id}!",
185
+ input,
186
+ timestamp: new Date().toISOString(),
187
+ });
188
+ })
189
+
190
+ // ➕ POST ${route.pattern}
191
+ .post(async (ctx) => {
192
+ const input = await ctx.input(contract, "POST", ctx.params);
193
+ // TODO: 계약의 응답 코드에 맞게 status를 조정하세요
194
+ return ctx.output(contract, 201, {
195
+ message: "Created!",
196
+ input,
197
+ timestamp: new Date().toISOString(),
198
+ });
199
+ });
200
+
201
+ // 💡 Contract 기반 사용법:
202
+ // ctx.input(contract, "GET") - Contract로 요청 검증 + 정규화
203
+ // ctx.output(contract, 200, data) - Contract로 응답 검증
204
+ // ctx.okContract(contract, data) - 200 OK (Contract 검증)
205
+ // ctx.createdContract(contract, data) - 201 Created (Contract 검증)
206
+ `;
207
+ }
159
208
 
160
209
  function computeSlotImportPath(slotModule: string, fromDir: string): string {
161
210
  // slotModule: "apps/server/slots/users.logic.ts"
@@ -334,9 +383,13 @@ export default {
334
383
  * "todo-page" → "TodoPage"
335
384
  * "user_profile" → "UserProfile"
336
385
  */
337
- function toPascalCase(str: string): string {
386
+ function toPascalCase(str: string): string {
338
387
  return str
339
388
  .split(/[-_]/)
340
389
  .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
341
390
  .join("");
342
- }
391
+ }
392
+
393
+ function pathDirname(filePath: string): string {
394
+ return filePath.replace(/\\/g, "/").split("/").slice(0, -1).join("/");
395
+ }
@@ -239,11 +239,16 @@ export function resolveImportLayer(
239
239
  export function extractSlice(filePath: string, layer: string): string | undefined {
240
240
  const relativePath = filePath.replace(/\\/g, "/");
241
241
 
242
- // layer/slice/... 형식에서 slice 추출
243
- const pattern = new RegExp(`${layer}/([^/]+)`);
244
- const match = relativePath.match(pattern);
242
+ const marker = `${layer}/`;
243
+ const index = relativePath.indexOf(marker);
244
+ if (index === -1) {
245
+ return undefined;
246
+ }
247
+
248
+ const rest = relativePath.slice(index + marker.length);
249
+ const slice = rest.split("/")[0];
245
250
 
246
- return match?.[1];
251
+ return slice || undefined;
247
252
  }
248
253
 
249
254
  // ═══════════════════════════════════════════════════════════════════════════
@@ -1,16 +1,46 @@
1
- import { GUARD_RULES, FORBIDDEN_IMPORTS, type GuardViolation } from "./rules";
2
- import { verifyLock, computeHash } from "../spec/lock";
3
- import { runContractGuardCheck } from "./contract-guard";
4
- import { validateSlotContent } from "../slot/validator";
5
- import type { RoutesManifest } from "../spec/schema";
6
- import type { GeneratedMap } from "../generator/generate";
7
- import path from "path";
8
- import fs from "fs/promises";
9
-
10
- export interface GuardCheckResult {
11
- passed: boolean;
12
- violations: GuardViolation[];
13
- }
1
+ import { GUARD_RULES, FORBIDDEN_IMPORTS, type GuardViolation } from "./rules";
2
+ import { verifyLock, computeHash } from "../spec/lock";
3
+ import { runContractGuardCheck } from "./contract-guard";
4
+ import { validateSlotContent } from "../slot/validator";
5
+ import type { RoutesManifest } from "../spec/schema";
6
+ import type { GeneratedMap } from "../generator/generate";
7
+ import { loadManduConfig, type GuardRuleSeverity } from "../config";
8
+ import path from "path";
9
+ import fs from "fs/promises";
10
+
11
+ export interface GuardCheckResult {
12
+ passed: boolean;
13
+ violations: GuardViolation[];
14
+ }
15
+
16
+ function normalizeSeverity(level: GuardRuleSeverity): "error" | "warning" | "off" {
17
+ if (level === "warn") return "warning";
18
+ return level;
19
+ }
20
+
21
+ function applyRuleSeverity(
22
+ violations: GuardViolation[],
23
+ config: { rules?: Record<string, GuardRuleSeverity>; contractRequired?: GuardRuleSeverity }
24
+ ): GuardViolation[] {
25
+ const resolved: GuardViolation[] = [];
26
+ const ruleOverrides = config.rules ?? {};
27
+
28
+ for (const violation of violations) {
29
+ let override = ruleOverrides[violation.ruleId];
30
+ if (violation.ruleId === "CONTRACT_MISSING" && config.contractRequired) {
31
+ override = config.contractRequired;
32
+ }
33
+
34
+ const baseSeverity = violation.severity ?? GUARD_RULES[violation.ruleId]?.severity ?? "error";
35
+ const finalSeverity = override ? normalizeSeverity(override) : baseSeverity;
36
+
37
+ if (finalSeverity === "off") continue;
38
+
39
+ resolved.push({ ...violation, severity: finalSeverity });
40
+ }
41
+
42
+ return resolved;
43
+ }
14
44
 
15
45
  async function fileExists(filePath: string): Promise<boolean> {
16
46
  try {
@@ -170,13 +200,15 @@ export async function checkSlotContentValidation(
170
200
  if (issue.severity === "error") {
171
201
  // Map slot issue codes to guard rule IDs
172
202
  let ruleId = "SLOT_VALIDATION_ERROR";
173
- if (issue.code === "MISSING_DEFAULT_EXPORT") {
174
- ruleId = GUARD_RULES.SLOT_MISSING_DEFAULT_EXPORT?.id ?? "SLOT_MISSING_DEFAULT_EXPORT";
175
- } else if (issue.code === "NO_RESPONSE_PATTERN" || issue.code === "INVALID_HANDLER_RETURN") {
176
- ruleId = GUARD_RULES.SLOT_INVALID_RETURN?.id ?? "SLOT_INVALID_RETURN";
177
- } else if (issue.code === "MISSING_FILLING_PATTERN") {
178
- ruleId = GUARD_RULES.SLOT_MISSING_FILLING_PATTERN?.id ?? "SLOT_MISSING_FILLING_PATTERN";
179
- }
203
+ if (issue.code === "MISSING_DEFAULT_EXPORT") {
204
+ ruleId = GUARD_RULES.SLOT_MISSING_DEFAULT_EXPORT?.id ?? "SLOT_MISSING_DEFAULT_EXPORT";
205
+ } else if (issue.code === "NO_RESPONSE_PATTERN" || issue.code === "INVALID_HANDLER_RETURN") {
206
+ ruleId = GUARD_RULES.SLOT_INVALID_RETURN?.id ?? "SLOT_INVALID_RETURN";
207
+ } else if (issue.code === "MISSING_FILLING_PATTERN") {
208
+ ruleId = GUARD_RULES.SLOT_MISSING_FILLING_PATTERN?.id ?? "SLOT_MISSING_FILLING_PATTERN";
209
+ } else if (issue.code === "ZOD_DIRECT_IMPORT") {
210
+ ruleId = GUARD_RULES.SLOT_ZOD_DIRECT_IMPORT?.id ?? "SLOT_ZOD_DIRECT_IMPORT";
211
+ }
180
212
 
181
213
  violations.push({
182
214
  ruleId,
@@ -332,11 +364,12 @@ export async function checkSpecDirNaming(
332
364
  return violations;
333
365
  }
334
366
 
335
- export async function runGuardCheck(
336
- manifest: RoutesManifest,
337
- rootDir: string
338
- ): Promise<GuardCheckResult> {
339
- const violations: GuardViolation[] = [];
367
+ export async function runGuardCheck(
368
+ manifest: RoutesManifest,
369
+ rootDir: string
370
+ ): Promise<GuardCheckResult> {
371
+ const violations: GuardViolation[] = [];
372
+ const config = await loadManduConfig(rootDir);
340
373
 
341
374
  const lockPath = path.join(rootDir, "spec/spec.lock.json");
342
375
  const mapPath = path.join(rootDir, "packages/core/map/generated.map.json");
@@ -392,8 +425,11 @@ export async function runGuardCheck(
392
425
  const specDirViolations = await checkSpecDirNaming(rootDir);
393
426
  violations.push(...specDirViolations);
394
427
 
395
- return {
396
- passed: violations.length === 0,
397
- violations,
398
- };
399
- }
428
+ const resolvedViolations = applyRuleSeverity(violations, config.guard ?? {});
429
+ const passed = resolvedViolations.every((v) => v.severity !== "error");
430
+
431
+ return {
432
+ passed,
433
+ violations: resolvedViolations,
434
+ };
435
+ }
@@ -199,15 +199,15 @@ export async function checkContractSlotConsistency(
199
199
  /**
200
200
  * Run all contract-related guard checks
201
201
  */
202
- export async function runContractGuardCheck(
203
- manifest: RoutesManifest,
204
- rootDir: string
205
- ): Promise<ContractViolation[]> {
206
- const violations: ContractViolation[] = [];
207
-
208
- // Check missing contracts (warning level)
209
- // const missingContracts = await checkMissingContract(manifest, rootDir);
210
- // violations.push(...missingContracts);
202
+ export async function runContractGuardCheck(
203
+ manifest: RoutesManifest,
204
+ rootDir: string
205
+ ): Promise<ContractViolation[]> {
206
+ const violations: ContractViolation[] = [];
207
+
208
+ // Check missing contracts (warning level)
209
+ const missingContracts = await checkMissingContract(manifest, rootDir);
210
+ violations.push(...missingContracts);
211
211
 
212
212
  // Check contract file exists
213
213
  const notFoundContracts = await checkContractFileExists(manifest, rootDir);
@@ -0,0 +1,24 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { validateFileAnalysis } from "./validator";
3
+ import { fsdPreset } from "./presets/fsd";
4
+ import type { FileAnalysis, GuardConfig } from "./types";
5
+
6
+ describe("TypeScript-only rule", () => {
7
+ const layers = fsdPreset.layers;
8
+ const config: GuardConfig = {
9
+ preset: "fsd",
10
+ severity: { fileType: "error" },
11
+ };
12
+
13
+ it("should flag .jsx files", () => {
14
+ const analysis: FileAnalysis = {
15
+ filePath: "src/pages/home.jsx",
16
+ layer: "pages",
17
+ imports: [],
18
+ analyzedAt: Date.now(),
19
+ };
20
+
21
+ const violations = validateFileAnalysis(analysis, layers, config);
22
+ expect(violations.some((v) => v.type === "file-type")).toBe(true);
23
+ });
24
+ });