@mandujs/core 0.4.2 → 0.5.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.
@@ -0,0 +1,221 @@
1
+ /**
2
+ * Mandu Contract Guard
3
+ * Contract-Slot 일관성 검사
4
+ */
5
+
6
+ import type { RoutesManifest } from "../spec/schema";
7
+ import type { GuardViolation } from "./rules";
8
+ import path from "path";
9
+ import fs from "fs/promises";
10
+
11
+ async function fileExists(filePath: string): Promise<boolean> {
12
+ try {
13
+ await fs.access(filePath);
14
+ return true;
15
+ } catch {
16
+ return false;
17
+ }
18
+ }
19
+
20
+ async function readFileContent(filePath: string): Promise<string | null> {
21
+ try {
22
+ return await Bun.file(filePath).text();
23
+ } catch {
24
+ return null;
25
+ }
26
+ }
27
+
28
+ /**
29
+ * Extract HTTP methods from contract file content
30
+ * Looks for patterns like: GET: {, POST: {, etc.
31
+ */
32
+ function extractContractMethods(content: string): string[] {
33
+ const methods: string[] = [];
34
+ const methodPattern = /\b(GET|POST|PUT|PATCH|DELETE)\s*:\s*\{/g;
35
+ let match;
36
+
37
+ while ((match = methodPattern.exec(content)) !== null) {
38
+ if (!methods.includes(match[1])) {
39
+ methods.push(match[1]);
40
+ }
41
+ }
42
+
43
+ return methods;
44
+ }
45
+
46
+ /**
47
+ * Extract HTTP methods from slot file content
48
+ * Looks for patterns like: .get(, .post(, etc.
49
+ */
50
+ function extractSlotMethods(content: string): string[] {
51
+ const methods: string[] = [];
52
+ const methodPattern = /\.(get|post|put|patch|delete)\s*\(/gi;
53
+ let match;
54
+
55
+ while ((match = methodPattern.exec(content)) !== null) {
56
+ const method = match[1].toUpperCase();
57
+ if (!methods.includes(method)) {
58
+ methods.push(method);
59
+ }
60
+ }
61
+
62
+ return methods;
63
+ }
64
+
65
+ /**
66
+ * Contract-Slot consistency violations
67
+ */
68
+ export interface ContractViolation extends GuardViolation {
69
+ routeId: string;
70
+ contractPath?: string;
71
+ slotPath?: string;
72
+ missingMethods?: string[];
73
+ undocumentedMethods?: string[];
74
+ }
75
+
76
+ /**
77
+ * Check if API route has a contract defined
78
+ * Rule: API routes should have contracts for type safety
79
+ */
80
+ export async function checkMissingContract(
81
+ manifest: RoutesManifest,
82
+ rootDir: string
83
+ ): Promise<ContractViolation[]> {
84
+ const violations: ContractViolation[] = [];
85
+
86
+ for (const route of manifest.routes) {
87
+ // Only check API routes
88
+ if (route.kind !== "api") continue;
89
+
90
+ // Skip if no slot (simple routes don't need contracts)
91
+ if (!route.slotModule) continue;
92
+
93
+ // Check if contract is defined
94
+ if (!route.contractModule) {
95
+ violations.push({
96
+ ruleId: "CONTRACT_MISSING",
97
+ routeId: route.id,
98
+ file: route.slotModule,
99
+ message: `API 라우트 "${route.id}"에 contract가 정의되지 않았습니다`,
100
+ suggestion: `spec/contracts/${route.id}.contract.ts 파일을 생성하고 manifest에 contractModule을 추가하세요`,
101
+ });
102
+ }
103
+ }
104
+
105
+ return violations;
106
+ }
107
+
108
+ /**
109
+ * Check if contract file exists
110
+ */
111
+ export async function checkContractFileExists(
112
+ manifest: RoutesManifest,
113
+ rootDir: string
114
+ ): Promise<ContractViolation[]> {
115
+ const violations: ContractViolation[] = [];
116
+
117
+ for (const route of manifest.routes) {
118
+ if (route.contractModule) {
119
+ const contractPath = path.join(rootDir, route.contractModule);
120
+ const exists = await fileExists(contractPath);
121
+
122
+ if (!exists) {
123
+ violations.push({
124
+ ruleId: "CONTRACT_NOT_FOUND",
125
+ routeId: route.id,
126
+ file: route.contractModule,
127
+ contractPath: route.contractModule,
128
+ message: `Contract 파일을 찾을 수 없습니다 (routeId: ${route.id})`,
129
+ suggestion: "mandu generate를 실행하여 contract 파일을 생성하세요",
130
+ });
131
+ }
132
+ }
133
+ }
134
+
135
+ return violations;
136
+ }
137
+
138
+ /**
139
+ * Check Contract-Slot method consistency
140
+ * - Contract에 정의된 메서드는 Slot에 구현되어야 함
141
+ * - Slot에 구현된 메서드는 Contract에 정의되어야 함
142
+ */
143
+ export async function checkContractSlotConsistency(
144
+ manifest: RoutesManifest,
145
+ rootDir: string
146
+ ): Promise<ContractViolation[]> {
147
+ const violations: ContractViolation[] = [];
148
+
149
+ for (const route of manifest.routes) {
150
+ // Need both contract and slot for consistency check
151
+ if (!route.contractModule || !route.slotModule) continue;
152
+
153
+ const contractPath = path.join(rootDir, route.contractModule);
154
+ const slotPath = path.join(rootDir, route.slotModule);
155
+
156
+ const contractContent = await readFileContent(contractPath);
157
+ const slotContent = await readFileContent(slotPath);
158
+
159
+ // Skip if files don't exist (other rules will catch this)
160
+ if (!contractContent || !slotContent) continue;
161
+
162
+ const contractMethods = extractContractMethods(contractContent);
163
+ const slotMethods = extractSlotMethods(slotContent);
164
+
165
+ // Check for methods in contract but not in slot
166
+ const missingInSlot = contractMethods.filter((m) => !slotMethods.includes(m));
167
+ if (missingInSlot.length > 0) {
168
+ violations.push({
169
+ ruleId: "CONTRACT_METHOD_NOT_IMPLEMENTED",
170
+ routeId: route.id,
171
+ file: route.slotModule,
172
+ contractPath: route.contractModule,
173
+ slotPath: route.slotModule,
174
+ missingMethods: missingInSlot,
175
+ message: `Contract에 정의된 메서드가 Slot에 구현되지 않았습니다: ${missingInSlot.join(", ")}`,
176
+ suggestion: `${route.slotModule}에 .${missingInSlot.map((m) => m.toLowerCase()).join("(), .")}() 핸들러를 추가하세요`,
177
+ });
178
+ }
179
+
180
+ // Check for methods in slot but not in contract (warning)
181
+ const undocumented = slotMethods.filter((m) => !contractMethods.includes(m));
182
+ if (undocumented.length > 0) {
183
+ violations.push({
184
+ ruleId: "CONTRACT_METHOD_UNDOCUMENTED",
185
+ routeId: route.id,
186
+ file: route.contractModule,
187
+ contractPath: route.contractModule,
188
+ slotPath: route.slotModule,
189
+ undocumentedMethods: undocumented,
190
+ message: `Slot에 구현된 메서드가 Contract에 문서화되지 않았습니다: ${undocumented.join(", ")}`,
191
+ suggestion: `${route.contractModule}에 ${undocumented.join(", ")} 스키마를 추가하세요`,
192
+ });
193
+ }
194
+ }
195
+
196
+ return violations;
197
+ }
198
+
199
+ /**
200
+ * Run all contract-related guard checks
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);
211
+
212
+ // Check contract file exists
213
+ const notFoundContracts = await checkContractFileExists(manifest, rootDir);
214
+ violations.push(...notFoundContracts);
215
+
216
+ // Check contract-slot consistency
217
+ const consistencyViolations = await checkContractSlotConsistency(manifest, rootDir);
218
+ violations.push(...consistencyViolations);
219
+
220
+ return violations;
221
+ }
@@ -1,3 +1,4 @@
1
1
  export * from "./rules";
2
2
  export * from "./check";
3
3
  export * from "./auto-correct";
4
+ export * from "./contract-guard";
@@ -37,6 +37,27 @@ export const GUARD_RULES: Record<string, GuardRule> = {
37
37
  name: "Slot File Not Found",
38
38
  description: "spec에 명시된 slotModule 파일을 찾을 수 없습니다",
39
39
  },
40
+ // Contract-related rules
41
+ CONTRACT_MISSING: {
42
+ id: "CONTRACT_MISSING",
43
+ name: "Contract Missing",
44
+ description: "API 라우트에 contract가 정의되지 않았습니다",
45
+ },
46
+ CONTRACT_NOT_FOUND: {
47
+ id: "CONTRACT_NOT_FOUND",
48
+ name: "Contract File Not Found",
49
+ description: "spec에 명시된 contractModule 파일을 찾을 수 없습니다",
50
+ },
51
+ CONTRACT_METHOD_NOT_IMPLEMENTED: {
52
+ id: "CONTRACT_METHOD_NOT_IMPLEMENTED",
53
+ name: "Contract Method Not Implemented",
54
+ description: "Contract에 정의된 메서드가 Slot에 구현되지 않았습니다",
55
+ },
56
+ CONTRACT_METHOD_UNDOCUMENTED: {
57
+ id: "CONTRACT_METHOD_UNDOCUMENTED",
58
+ name: "Contract Method Undocumented",
59
+ description: "Slot에 구현된 메서드가 Contract에 문서화되지 않았습니다",
60
+ },
40
61
  };
41
62
 
42
63
  export const FORBIDDEN_IMPORTS = ["fs", "child_process", "cluster", "worker_threads"];
package/src/index.ts CHANGED
@@ -8,3 +8,5 @@ export * from "./change";
8
8
  export * from "./error";
9
9
  export * from "./slot";
10
10
  export * from "./bundler";
11
+ export * from "./contract";
12
+ export * from "./openapi";