@mandujs/core 0.9.14 → 0.9.16

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.14",
3
+ "version": "0.9.16",
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
  /**
@@ -5,6 +5,7 @@ import { computeHash } from "../spec/lock";
5
5
  import { getWatcher } from "../watcher/watcher";
6
6
  import path from "path";
7
7
  import fs from "fs/promises";
8
+ import fsSync from "fs";
8
9
 
9
10
  async function fileExists(filePath: string): Promise<boolean> {
10
11
  try {
@@ -345,6 +346,12 @@ export async function generateRoutes(
345
346
 
346
347
  // Resume watcher after generation
347
348
  watcher?.resume();
349
+ // Cross-process timestamp: watcher skips warnings if generate finished recently
350
+ const stampDir = path.join(rootDir, ".mandu");
351
+ if (!fsSync.existsSync(stampDir)) {
352
+ fsSync.mkdirSync(stampDir, { recursive: true });
353
+ }
354
+ fsSync.writeFileSync(path.join(stampDir, "generate.stamp"), Date.now().toString());
348
355
 
349
356
  return result;
350
357
  }
@@ -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
 
@@ -285,6 +285,20 @@ export class FileWatcher {
285
285
 
286
286
  const { rootDir } = this.config;
287
287
 
288
+ // Cross-process: skip if generate finished within last 2 seconds
289
+ // Walk up from the changed file to find nearest .mandu/generate.stamp
290
+ let stampDir = path.dirname(filePath);
291
+ while (stampDir !== path.dirname(stampDir)) {
292
+ const stampFile = path.join(stampDir, ".mandu", "generate.stamp");
293
+ try {
294
+ const stamp = parseInt(fs.readFileSync(stampFile, "utf-8"), 10);
295
+ if (Date.now() - stamp < 2000) return;
296
+ break;
297
+ } catch {}
298
+ stampDir = path.dirname(stampDir);
299
+ }
300
+
301
+
288
302
  // Validate file against rules
289
303
  try {
290
304
  const warnings = await validateFile(filePath, event, rootDir);