@mandujs/core 0.9.15 → 0.9.17

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mandujs/core",
3
- "version": "0.9.15",
3
+ "version": "0.9.17",
4
4
  "description": "Mandu Framework Core - Spec, Generator, Guard, Runtime",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -146,6 +146,12 @@ export interface ArchRule {
146
146
  forbiddenImports?: string[];
147
147
  /** Optional: required patterns in content */
148
148
  requiredPatterns?: RegExp[];
149
+ /** Optional: exclude paths matching this pattern */
150
+ excludePattern?: string;
151
+ /** Recommended action for the agent */
152
+ agentAction?: "regenerate" | "move" | "rename" | "remove_import" | "validate" | "none";
153
+ /** MCP tool to execute */
154
+ agentCommand?: string;
149
155
  }
150
156
 
151
157
  /**
@@ -164,6 +170,10 @@ export interface WatchWarning {
164
170
  event: "create" | "modify" | "delete";
165
171
  /** Warning level */
166
172
  level?: "info" | "warn";
173
+ /** Recommended action for the agent */
174
+ agentAction?: string;
175
+ /** MCP tool to execute */
176
+ agentCommand?: string;
167
177
  }
168
178
 
169
179
  /**
@@ -291,6 +291,47 @@ async function scanTsFiles(dir: string): Promise<string[]> {
291
291
  return files;
292
292
  }
293
293
 
294
+ // Rule: spec/ directory naming convention scan
295
+ export async function checkSpecDirNaming(
296
+ rootDir: string
297
+ ): Promise<GuardViolation[]> {
298
+ const violations: GuardViolation[] = [];
299
+
300
+ // Check spec/slots/ — only .slot.ts allowed
301
+ const slotsDir = path.join(rootDir, "spec/slots");
302
+ try {
303
+ const files = await fs.readdir(slotsDir);
304
+ for (const file of files) {
305
+ if (file.endsWith(".ts") && !file.endsWith(".slot.ts")) {
306
+ violations.push({
307
+ ruleId: GUARD_RULES.SLOT_DIR_INVALID_FILE.id,
308
+ file: `spec/slots/${file}`,
309
+ message: `spec/slots/에 .slot.ts가 아닌 파일: ${file}`,
310
+ suggestion: `.slot.ts로 이름을 바꾸거나, 이 파일이 client slot이면 apps/web/components/로 이동하세요`,
311
+ });
312
+ }
313
+ }
314
+ } catch {}
315
+
316
+ // Check spec/contracts/ — only .contract.ts allowed
317
+ const contractsDir = path.join(rootDir, "spec/contracts");
318
+ try {
319
+ const files = await fs.readdir(contractsDir);
320
+ for (const file of files) {
321
+ if (file.endsWith(".ts") && !file.endsWith(".contract.ts")) {
322
+ violations.push({
323
+ ruleId: GUARD_RULES.CONTRACT_DIR_INVALID_FILE.id,
324
+ file: `spec/contracts/${file}`,
325
+ message: `spec/contracts/에 .contract.ts가 아닌 파일: ${file}`,
326
+ suggestion: `.contract.ts로 이름을 바꾸세요`,
327
+ });
328
+ }
329
+ }
330
+ } catch {}
331
+
332
+ return violations;
333
+ }
334
+
294
335
  export async function runGuardCheck(
295
336
  manifest: RoutesManifest,
296
337
  rootDir: string
@@ -347,6 +388,10 @@ export async function runGuardCheck(
347
388
  const islandViolations = await checkIslandFirstIntegrity(manifest, rootDir);
348
389
  violations.push(...islandViolations);
349
390
 
391
+ // Rule: spec/ directory naming convention
392
+ const specDirViolations = await checkSpecDirNaming(rootDir);
393
+ violations.push(...specDirViolations);
394
+
350
395
  return {
351
396
  passed: violations.length === 0,
352
397
  violations,
@@ -108,6 +108,18 @@ export const GUARD_RULES: Record<string, GuardRule> = {
108
108
  description: "spec에 명시된 clientModule 파일을 찾을 수 없습니다",
109
109
  severity: "error",
110
110
  },
111
+ SLOT_DIR_INVALID_FILE: {
112
+ id: "SLOT_DIR_INVALID_FILE",
113
+ name: "Invalid File in Slots Directory",
114
+ description: "spec/slots/ 디렉토리에 .slot.ts가 아닌 파일이 있습니다",
115
+ severity: "error",
116
+ },
117
+ CONTRACT_DIR_INVALID_FILE: {
118
+ id: "CONTRACT_DIR_INVALID_FILE",
119
+ name: "Invalid File in Contracts Directory",
120
+ description: "spec/contracts/ 디렉토리에 .contract.ts가 아닌 파일이 있습니다",
121
+ severity: "error",
122
+ },
111
123
  };
112
124
 
113
125
  export const FORBIDDEN_IMPORTS = ["fs", "child_process", "cluster", "worker_threads"];
@@ -27,14 +27,19 @@ export const MVP_RULES: ArchRule[] = [
27
27
  pattern: "generated/**",
28
28
  action: "warn",
29
29
  message: "Generated 파일이 직접 수정되었습니다. 이 파일은 `mandu generate`로 재생성됩니다.",
30
+ agentAction: "regenerate",
31
+ agentCommand: "mandu_generate",
30
32
  },
31
33
  {
32
34
  id: "WRONG_SLOT_LOCATION",
33
35
  name: "Wrong Slot Location",
34
36
  description: "Slot 파일은 spec/slots/ 디렉토리에 있어야 합니다",
35
- pattern: "src/**/*.slot.ts",
37
+ pattern: "**/*.slot.ts",
36
38
  action: "warn",
37
39
  message: "Slot 파일이 잘못된 위치에 있습니다. spec/slots/ 디렉토리로 이동하세요.",
40
+ excludePattern: "spec/slots/**",
41
+ agentAction: "move",
42
+ agentCommand: "mandu_check_location",
38
43
  },
39
44
  {
40
45
  id: "SLOT_NAMING",
@@ -44,6 +49,8 @@ export const MVP_RULES: ArchRule[] = [
44
49
  action: "warn",
45
50
  message: "Slot 파일명이 .slot.ts로 끝나야 합니다.",
46
51
  mustEndWith: ".slot.ts",
52
+ agentAction: "rename",
53
+ agentCommand: "mandu_check_location",
47
54
  },
48
55
  {
49
56
  id: "CONTRACT_NAMING",
@@ -53,6 +60,8 @@ export const MVP_RULES: ArchRule[] = [
53
60
  action: "warn",
54
61
  message: "Contract 파일명이 .contract.ts로 끝나야 합니다.",
55
62
  mustEndWith: ".contract.ts",
63
+ agentAction: "rename",
64
+ agentCommand: "mandu_check_location",
56
65
  },
57
66
  {
58
67
  id: "FORBIDDEN_IMPORT",
@@ -62,6 +71,8 @@ export const MVP_RULES: ArchRule[] = [
62
71
  action: "warn",
63
72
  message: "Generated 파일에서 금지된 모듈이 import되었습니다.",
64
73
  forbiddenImports: ["fs", "child_process", "cluster", "worker_threads"],
74
+ agentAction: "remove_import",
75
+ agentCommand: "mandu_guard_check",
65
76
  },
66
77
  {
67
78
  id: "SLOT_MODIFIED",
@@ -70,6 +81,8 @@ export const MVP_RULES: ArchRule[] = [
70
81
  pattern: "spec/slots/*.slot.ts",
71
82
  action: "warn",
72
83
  message: "Slot 수정 감지. mandu_validate_slot 또는 mandu_guard_check로 검증하세요.",
84
+ agentAction: "validate",
85
+ agentCommand: "mandu_validate_slot",
73
86
  },
74
87
  {
75
88
  id: "ISLAND_FIRST_MODIFIED",
@@ -78,6 +91,8 @@ export const MVP_RULES: ArchRule[] = [
78
91
  pattern: "apps/web/generated/routes/**",
79
92
  action: "warn",
80
93
  message: "Island-First componentModule이 수동 수정되었습니다. mandu generate를 실행하세요.",
94
+ agentAction: "regenerate",
95
+ agentCommand: "mandu_generate",
81
96
  },
82
97
  ];
83
98
 
@@ -123,6 +138,10 @@ export function matchRules(filePath: string): ArchRule[] {
123
138
 
124
139
  for (const rule of MVP_RULES) {
125
140
  if (matchGlob(rule.pattern, filePath)) {
141
+ // Skip if excluded
142
+ if (rule.excludePattern && matchGlob(rule.excludePattern, filePath)) {
143
+ continue;
144
+ }
126
145
  matched.push(rule);
127
146
  }
128
147
  }
@@ -193,15 +212,19 @@ export async function validateFile(
193
212
  continue;
194
213
  }
195
214
 
215
+ // Base warning fields reused across all branches
216
+ const base = {
217
+ ruleId: rule.id,
218
+ file: relativePath,
219
+ timestamp: new Date(),
220
+ event,
221
+ agentAction: rule.agentAction,
222
+ agentCommand: rule.agentCommand,
223
+ } as const;
224
+
196
225
  // Check naming convention
197
226
  if (rule.mustEndWith && !checkNamingConvention(relativePath, rule)) {
198
- warnings.push({
199
- ruleId: rule.id,
200
- file: relativePath,
201
- message: rule.message,
202
- timestamp: new Date(),
203
- event,
204
- });
227
+ warnings.push({ ...base, message: rule.message });
205
228
  continue;
206
229
  }
207
230
 
@@ -221,11 +244,8 @@ export async function validateFile(
221
244
 
222
245
  if (forbidden.length > 0) {
223
246
  warnings.push({
224
- ruleId: rule.id,
225
- file: relativePath,
247
+ ...base,
226
248
  message: `${rule.message} (${forbidden.join(", ")})`,
227
- timestamp: new Date(),
228
- event,
229
249
  });
230
250
  }
231
251
  } catch {
@@ -236,25 +256,12 @@ export async function validateFile(
236
256
 
237
257
  // Default: generate warning for pattern match
238
258
  if (rule.id === "GENERATED_DIRECT_EDIT" || rule.id === "WRONG_SLOT_LOCATION" || rule.id === "ISLAND_FIRST_MODIFIED") {
239
- warnings.push({
240
- ruleId: rule.id,
241
- file: relativePath,
242
- message: rule.message,
243
- timestamp: new Date(),
244
- event,
245
- });
259
+ warnings.push({ ...base, message: rule.message });
246
260
  }
247
261
 
248
262
  // Slot modified: info level notification
249
263
  if (rule.id === "SLOT_MODIFIED" && event !== "delete") {
250
- warnings.push({
251
- ruleId: rule.id,
252
- file: relativePath,
253
- message: rule.message,
254
- timestamp: new Date(),
255
- event,
256
- level: "info",
257
- });
264
+ warnings.push({ ...base, message: rule.message, level: "info" as const });
258
265
  }
259
266
  }
260
267