@mandujs/mcp 0.37.2 → 0.37.3
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 +2 -2
- package/src/resources/skills/guides.ts +49 -40
- package/src/resources/skills/mandu-hydration/SKILL.md +19 -11
- package/src/resources/skills/mandu-hydration/rules/hydration-island-setup.md +54 -7
- package/src/resources/skills/mandu-hydration/rules/hydration-priority-visible.md +60 -37
- package/src/resources/skills/recipes.ts +28 -19
- package/src/tools/guard.ts +162 -68
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mandujs/mcp",
|
|
3
|
-
"version": "0.37.
|
|
3
|
+
"version": "0.37.3",
|
|
4
4
|
"description": "Mandu MCP Server - Agent-native interface for Mandu framework operations",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"access": "public"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@mandujs/core": "^0.54.
|
|
37
|
+
"@mandujs/core": "^0.54.3",
|
|
38
38
|
"@mandujs/ate": "^0.26.1",
|
|
39
39
|
"@mandujs/skills": "^0.20.1",
|
|
40
40
|
"@modelcontextprotocol/sdk": "^1.25.3"
|
|
@@ -377,21 +377,21 @@ Island Hydration은 페이지의 일부분만 클라이언트에서 인터랙티
|
|
|
377
377
|
| \`idle\` | 브라우저 유휴 시 | 비중요 기능 |
|
|
378
378
|
| \`interaction\` | 사용자 상호작용 시 | 클릭해야 활성화 |
|
|
379
379
|
|
|
380
|
-
##
|
|
381
|
-
|
|
382
|
-
### 1. 클라이언트 컴포넌트 작성
|
|
383
|
-
|
|
384
|
-
\`\`\`tsx
|
|
385
|
-
// app/counter/client.tsx
|
|
386
|
-
|
|
387
|
-
"use client";
|
|
388
|
-
|
|
389
|
-
import { useState } from "react";
|
|
390
|
-
|
|
391
|
-
export
|
|
392
|
-
const [count, setCount] = useState(initial);
|
|
393
|
-
|
|
394
|
-
return (
|
|
380
|
+
## Inline client region 만들기
|
|
381
|
+
|
|
382
|
+
### 1. 클라이언트 컴포넌트 작성
|
|
383
|
+
|
|
384
|
+
\`\`\`tsx
|
|
385
|
+
// app/counter/client.tsx
|
|
386
|
+
|
|
387
|
+
"use client";
|
|
388
|
+
|
|
389
|
+
import { useState } from "react";
|
|
390
|
+
|
|
391
|
+
export function Counter({ initial = 0 }: { initial?: number }) {
|
|
392
|
+
const [count, setCount] = useState(initial);
|
|
393
|
+
|
|
394
|
+
return (
|
|
395
395
|
<div>
|
|
396
396
|
<p>Count: {count}</p>
|
|
397
397
|
<button onClick={() => setCount(c => c - 1)}>-</button>
|
|
@@ -399,34 +399,43 @@ export default function Counter({ initial = 0 }: { initial?: number }) {
|
|
|
399
399
|
</div>
|
|
400
400
|
);
|
|
401
401
|
}
|
|
402
|
-
\`\`\`
|
|
403
|
-
|
|
404
|
-
### 2. 페이지에서 사용
|
|
405
|
-
|
|
406
|
-
\`\`\`tsx
|
|
407
|
-
// app/counter/page.tsx
|
|
408
|
-
|
|
409
|
-
import
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
402
|
+
\`\`\`
|
|
403
|
+
|
|
404
|
+
### 2. 서버 페이지에서 partial로 사용
|
|
405
|
+
|
|
406
|
+
\`\`\`tsx
|
|
407
|
+
// app/counter/page.tsx
|
|
408
|
+
|
|
409
|
+
import { partial } from "@mandujs/core/client";
|
|
410
|
+
import { Counter } from "./client";
|
|
411
|
+
|
|
412
|
+
const CounterPartial = partial({
|
|
413
|
+
component: Counter,
|
|
414
|
+
priority: "visible",
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
export default function CounterPage() {
|
|
418
|
+
return (
|
|
413
419
|
<div>
|
|
414
420
|
<h1>Counter Demo</h1>
|
|
415
421
|
<p>이 텍스트는 정적 HTML입니다.</p>
|
|
416
|
-
|
|
417
|
-
{/* 이 부분만 hydration됩니다 */}
|
|
418
|
-
<
|
|
419
|
-
</div>
|
|
420
|
-
);
|
|
421
|
-
}
|
|
422
|
-
\`\`\`
|
|
423
|
-
|
|
424
|
-
## Mandu.island() API
|
|
425
|
-
|
|
426
|
-
고급 Island 패턴을 위한 API
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
422
|
+
|
|
423
|
+
{/* 이 부분만 hydration됩니다 */}
|
|
424
|
+
<CounterPartial.Render initial={10} />
|
|
425
|
+
</div>
|
|
426
|
+
);
|
|
427
|
+
}
|
|
428
|
+
\`\`\`
|
|
429
|
+
|
|
430
|
+
## Mandu.island() API
|
|
431
|
+
|
|
432
|
+
고급 page-level Island 패턴을 위한 API입니다. \`Mandu.island()\`는 단일
|
|
433
|
+
정의 객체만 받습니다. \`island("visible", Component)\` 형태는 지원하지
|
|
434
|
+
않으며, 서버 페이지 안에 inline으로 렌더링할 영역은 \`partial()\`을
|
|
435
|
+
사용하세요.
|
|
436
|
+
|
|
437
|
+
\`\`\`typescript
|
|
438
|
+
// spec/slots/todos.client.ts
|
|
430
439
|
|
|
431
440
|
import { Mandu } from "@mandujs/core/client";
|
|
432
441
|
import { useState, useCallback } from "react";
|
|
@@ -57,17 +57,25 @@ Reference these guidelines when:
|
|
|
57
57
|
- `hydration-data-server` - Access server data with useServerData
|
|
58
58
|
- `hydration-data-event` - Communicate between Islands with useIslandEvent
|
|
59
59
|
|
|
60
|
-
## Hydration Strategies
|
|
61
|
-
|
|
62
|
-
| Strategy | Description | Use Case |
|
|
63
|
-
|----------|-------------|----------|
|
|
64
|
-
| `none` | No JavaScript | Pure static pages |
|
|
65
|
-
| `island` | Partial hydration (default) | Static + interactive mix |
|
|
66
|
-
| `full` | Full hydration | SPA-style pages |
|
|
67
|
-
|
|
68
|
-
##
|
|
69
|
-
|
|
70
|
-
|
|
60
|
+
## Hydration Strategies
|
|
61
|
+
|
|
62
|
+
| Strategy | Description | Use Case |
|
|
63
|
+
|----------|-------------|----------|
|
|
64
|
+
| `none` | No JavaScript | Pure static pages |
|
|
65
|
+
| `island` | Partial hydration (default) | Static + interactive mix |
|
|
66
|
+
| `full` | Full hydration | SPA-style pages |
|
|
67
|
+
|
|
68
|
+
## Runtime API Constraints
|
|
69
|
+
|
|
70
|
+
- `island()` / `Mandu.island()` takes one definition object: `island({ setup, render })`.
|
|
71
|
+
- Do not call `island("visible", Component)`; use `wrapComponent(Component)` for a simple page-level island wrapper.
|
|
72
|
+
- Islands are page-level client bundles. Do not render them as inline JSX like `<MyIsland />`.
|
|
73
|
+
- For an embedded interactive region inside a server page, use `partial()`: put it in `*.partial.tsx`, export `partial({ id, component })`, and render the returned `.Render` component from the server page.
|
|
74
|
+
- A server page that renders partials must opt into hydration, for example `export const hydration = { strategy: "island", priority: "visible", preload: false }`.
|
|
75
|
+
|
|
76
|
+
## Client Hooks
|
|
77
|
+
|
|
78
|
+
```typescript
|
|
71
79
|
import {
|
|
72
80
|
useServerData,
|
|
73
81
|
useHydrated,
|
|
@@ -5,13 +5,60 @@ impactDescription: Proper Island structure
|
|
|
5
5
|
tags: hydration, island, setup
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
-
## Use Mandu.island() with Setup Function
|
|
9
|
-
|
|
10
|
-
For complex Islands, use `Mandu.island()` API to separate state logic from rendering.
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
8
|
+
## Use Mandu.island() with Setup Function
|
|
9
|
+
|
|
10
|
+
For complex Islands, use `Mandu.island()` API to separate state logic from rendering.
|
|
11
|
+
`Mandu.island()` takes a single definition object. The shorthand
|
|
12
|
+
`island("visible", Component)` is not a supported Mandu runtime API.
|
|
13
|
+
|
|
14
|
+
Islands are page-level client bundles. They are discovered and rendered by
|
|
15
|
+
the framework wrapper, not embedded directly as inline JSX. Use `partial()`
|
|
16
|
+
when a server page needs an inline interactive region.
|
|
17
|
+
|
|
18
|
+
```tsx
|
|
19
|
+
// Simple page-level island wrapper
|
|
20
|
+
"use client";
|
|
21
|
+
|
|
22
|
+
import { wrapComponent } from "@mandujs/core/client";
|
|
23
|
+
|
|
24
|
+
function Counter() {
|
|
25
|
+
return <button>Count</button>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export default wrapComponent(Counter);
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
```tsx
|
|
32
|
+
// Inline client region inside a server-rendered page
|
|
33
|
+
// app/Counter.partial.tsx
|
|
34
|
+
import { partial } from "@mandujs/core/client";
|
|
35
|
+
import { Counter } from "./counter.client";
|
|
36
|
+
|
|
37
|
+
export default partial({
|
|
38
|
+
id: "Counter",
|
|
39
|
+
component: Counter,
|
|
40
|
+
priority: "visible",
|
|
41
|
+
});
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
```tsx
|
|
45
|
+
// app/page.tsx
|
|
46
|
+
import CounterPartial from "./Counter.partial";
|
|
47
|
+
|
|
48
|
+
export const hydration = {
|
|
49
|
+
strategy: "island",
|
|
50
|
+
priority: "visible",
|
|
51
|
+
preload: false,
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export default function Page() {
|
|
55
|
+
return <CounterPartial.Render initial={0} />;
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
**Incorrect (mixed concerns):**
|
|
60
|
+
|
|
61
|
+
```tsx
|
|
15
62
|
"use client";
|
|
16
63
|
|
|
17
64
|
export default function TodoList({ initialTodos }) {
|
|
@@ -18,43 +18,66 @@ Set hydration priority based on when the component needs to be interactive.
|
|
|
18
18
|
| `idle` | Browser idle | Non-critical features (analytics widgets) |
|
|
19
19
|
| `interaction` | User action | Click-to-activate components |
|
|
20
20
|
|
|
21
|
-
## Examples
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
21
|
+
## Examples
|
|
22
|
+
|
|
23
|
+
Inline client regions live in `*.partial.tsx` files and use
|
|
24
|
+
`partial({ id, component, priority })`, then render the returned `.Render`
|
|
25
|
+
component from the server page. The page must export a non-`none` hydration
|
|
26
|
+
config.
|
|
27
|
+
|
|
28
|
+
### Immediate: Always-visible interactions
|
|
29
|
+
|
|
30
|
+
```tsx
|
|
31
|
+
// Header with navigation - needs to work immediately
|
|
32
|
+
import { partial } from "@mandujs/core/client";
|
|
33
|
+
|
|
34
|
+
const HeaderNavPartial = partial({
|
|
35
|
+
id: "HeaderNav",
|
|
36
|
+
component: HeaderNav,
|
|
37
|
+
priority: "immediate",
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
<HeaderNavPartial.Render />
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Visible: Below-fold content (default)
|
|
44
|
+
|
|
45
|
+
```tsx
|
|
46
|
+
// Comments section - load when scrolled into view
|
|
47
|
+
const CommentsPartial = partial({
|
|
48
|
+
id: "Comments",
|
|
49
|
+
component: CommentsSection,
|
|
50
|
+
priority: "visible",
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
<CommentsPartial.Render postId={postId} />
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Idle: Background features
|
|
57
|
+
|
|
58
|
+
```tsx
|
|
59
|
+
// Chat widget - can wait until browser is idle
|
|
60
|
+
const ChatPartial = partial({
|
|
61
|
+
id: "Chat",
|
|
62
|
+
component: ChatWidget,
|
|
63
|
+
priority: "idle",
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
<ChatPartial.Render />
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Interaction: On-demand activation
|
|
70
|
+
|
|
71
|
+
```tsx
|
|
72
|
+
// Video player - only hydrate when user clicks play
|
|
73
|
+
const VideoPartial = partial({
|
|
74
|
+
id: "Video",
|
|
75
|
+
component: VideoPlayer,
|
|
76
|
+
priority: "interaction",
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
<VideoPartial.Render videoId={videoId} />
|
|
80
|
+
```
|
|
58
81
|
|
|
59
82
|
## Performance Impact
|
|
60
83
|
|
|
@@ -469,24 +469,30 @@ export default function Counter({ initial = 0, step = 1 }: CounterProps) {
|
|
|
469
469
|
}
|
|
470
470
|
\`\`\`
|
|
471
471
|
|
|
472
|
-
## Step 2: 페이지에서 사용
|
|
473
|
-
|
|
474
|
-
\`\`\`tsx
|
|
475
|
-
// app/counter/page.tsx
|
|
476
|
-
|
|
477
|
-
import
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
472
|
+
## Step 2: 서버 페이지에서 partial로 사용
|
|
473
|
+
|
|
474
|
+
\`\`\`tsx
|
|
475
|
+
// app/counter/page.tsx
|
|
476
|
+
|
|
477
|
+
import { partial } from "@mandujs/core/client";
|
|
478
|
+
import Counter from "./client";
|
|
479
|
+
|
|
480
|
+
const CounterPartial = partial({
|
|
481
|
+
component: Counter,
|
|
482
|
+
priority: "visible",
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
export default function CounterPage() {
|
|
486
|
+
return (
|
|
481
487
|
<div style={{ padding: "20px" }}>
|
|
482
488
|
<h1>Counter Demo</h1>
|
|
483
489
|
<p>아래 카운터는 클라이언트에서 hydration됩니다.</p>
|
|
484
|
-
|
|
485
|
-
{/*
|
|
486
|
-
<
|
|
487
|
-
|
|
488
|
-
<p style={{ marginTop: "20px", color: "#666" }}>
|
|
489
|
-
이 텍스트는 정적 HTML입니다.
|
|
490
|
+
|
|
491
|
+
{/* Inline client region */}
|
|
492
|
+
<CounterPartial.Render initial={10} step={5} />
|
|
493
|
+
|
|
494
|
+
<p style={{ marginTop: "20px", color: "#666" }}>
|
|
495
|
+
이 텍스트는 정적 HTML입니다.
|
|
490
496
|
</p>
|
|
491
497
|
</div>
|
|
492
498
|
);
|
|
@@ -618,10 +624,13 @@ export default function UserList() {
|
|
|
618
624
|
|
|
619
625
|
## Mandu.island() API (고급)
|
|
620
626
|
|
|
621
|
-
서버 데이터와 클라이언트 상태를
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
627
|
+
서버 데이터와 클라이언트 상태를 분리하는 page-level Island가 필요하면
|
|
628
|
+
\`Mandu.island({ setup, render })\`를 사용합니다. \`island("visible",
|
|
629
|
+
Component)\`는 지원하지 않습니다. 서버 페이지 안에 inline으로 렌더링할
|
|
630
|
+
영역은 \`partial()\`을 사용하세요.
|
|
631
|
+
|
|
632
|
+
\`\`\`typescript
|
|
633
|
+
// spec/slots/todos.client.ts
|
|
625
634
|
|
|
626
635
|
import { Mandu } from "@mandujs/core/client";
|
|
627
636
|
import { useState } from "react";
|
package/src/tools/guard.ts
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import type { Tool } from "@modelcontextprotocol/sdk/types.js";
|
|
2
2
|
import { type ManduError } from "@mandujs/core/error";
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
import {
|
|
4
|
+
checkDirectory,
|
|
5
|
+
getDefaultFsRoutesGuardPolicy,
|
|
6
|
+
validateAndReport,
|
|
7
|
+
loadManifest,
|
|
8
|
+
runGuardCheck,
|
|
9
|
+
runAutoCorrect,
|
|
10
|
+
type GeneratedMap,
|
|
8
11
|
// Self-Healing Guard imports
|
|
9
12
|
checkWithHealing,
|
|
10
13
|
healAll,
|
|
@@ -12,16 +15,19 @@ import {
|
|
|
12
15
|
// Follow-up E — type-aware lint bridge
|
|
13
16
|
runTsgolint,
|
|
14
17
|
type GuardConfig,
|
|
15
|
-
type ViolationType,
|
|
16
|
-
type GuardPreset,
|
|
17
|
-
|
|
18
|
-
|
|
18
|
+
type ViolationType,
|
|
19
|
+
type GuardPreset,
|
|
20
|
+
type Violation,
|
|
21
|
+
} from "@mandujs/core";
|
|
22
|
+
import { getProjectPaths, readJsonFile, readConfig } from "../utils/project.js";
|
|
23
|
+
import fs from "fs/promises";
|
|
24
|
+
import path from "path";
|
|
19
25
|
|
|
20
26
|
export const guardToolDefinitions: Tool[] = [
|
|
21
27
|
{
|
|
22
|
-
name: "mandu.guard.check",
|
|
23
|
-
description:
|
|
24
|
-
"Run
|
|
28
|
+
name: "mandu.guard.check",
|
|
29
|
+
description:
|
|
30
|
+
"Run the same architecture guard used by `mandu guard`, plus legacy spec/generated/slot checks. Set typeAware=true to additionally run `oxlint --type-aware` (tsgolint) and merge its results.",
|
|
25
31
|
annotations: {
|
|
26
32
|
readOnlyHint: true,
|
|
27
33
|
},
|
|
@@ -120,8 +126,37 @@ export const guardToolDefinitions: Tool[] = [
|
|
|
120
126
|
},
|
|
121
127
|
];
|
|
122
128
|
|
|
123
|
-
export function guardTools(projectRoot: string) {
|
|
124
|
-
const paths = getProjectPaths(projectRoot);
|
|
129
|
+
export function guardTools(projectRoot: string) {
|
|
130
|
+
const paths = getProjectPaths(projectRoot);
|
|
131
|
+
|
|
132
|
+
const pathExists = async (candidate: string): Promise<boolean> => {
|
|
133
|
+
try {
|
|
134
|
+
await fs.access(candidate);
|
|
135
|
+
return true;
|
|
136
|
+
} catch {
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const summarizeArchitectureViolation = (violation: Violation) => ({
|
|
142
|
+
ruleId: violation.ruleName,
|
|
143
|
+
type: violation.type,
|
|
144
|
+
file: path.relative(projectRoot, violation.filePath).replace(/\\/g, "/") || violation.filePath,
|
|
145
|
+
line: violation.line,
|
|
146
|
+
column: violation.column,
|
|
147
|
+
message: violation.ruleDescription,
|
|
148
|
+
suggestion: violation.suggestions[0],
|
|
149
|
+
fromLayer: violation.fromLayer,
|
|
150
|
+
toLayer: violation.toLayer,
|
|
151
|
+
importStatement: violation.importStatement,
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
const summarizeLegacyViolation = (v: Awaited<ReturnType<typeof runGuardCheck>>["violations"][number]) => ({
|
|
155
|
+
ruleId: v.ruleId,
|
|
156
|
+
file: v.file,
|
|
157
|
+
message: v.message,
|
|
158
|
+
suggestion: v.suggestion,
|
|
159
|
+
});
|
|
125
160
|
|
|
126
161
|
const handlers: Record<string, (args: Record<string, unknown>) => Promise<unknown>> = {
|
|
127
162
|
"mandu.guard.check": async (args: Record<string, unknown>) => {
|
|
@@ -137,25 +172,44 @@ export function guardTools(projectRoot: string) {
|
|
|
137
172
|
error: "Failed to load manifest",
|
|
138
173
|
details: manifestResult.errors,
|
|
139
174
|
};
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
const
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const projectConfig = await validateAndReport(projectRoot);
|
|
178
|
+
const guardConfigFromFile = (projectConfig?.guard ?? {}) as GuardConfig;
|
|
179
|
+
const preset = guardConfigFromFile.preset ?? "mandu";
|
|
180
|
+
const enableFsRoutes = await pathExists(paths.appDir);
|
|
181
|
+
|
|
182
|
+
const architectureReport = await checkDirectory(
|
|
183
|
+
{
|
|
184
|
+
preset,
|
|
185
|
+
srcDir: guardConfigFromFile.srcDir ?? "src",
|
|
186
|
+
exclude: guardConfigFromFile.exclude,
|
|
187
|
+
fsRoutes: getDefaultFsRoutesGuardPolicy(enableFsRoutes),
|
|
188
|
+
},
|
|
189
|
+
projectRoot
|
|
190
|
+
);
|
|
191
|
+
const architecturePassed = architectureReport.bySeverity.error === 0;
|
|
192
|
+
const architectureViolations = architectureReport.violations.map(
|
|
193
|
+
summarizeArchitectureViolation
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
// Run guard check
|
|
197
|
+
const checkResult = await runGuardCheck(manifestResult.data, projectRoot);
|
|
144
198
|
|
|
145
199
|
// Follow-up E — resolve type-aware defaulting from config, then
|
|
146
200
|
// run the bridge when enabled. Result envelope is always included
|
|
147
201
|
// in the tool response so MCP clients have a single stable shape.
|
|
148
|
-
let
|
|
149
|
-
try {
|
|
150
|
-
|
|
151
|
-
} catch {
|
|
152
|
-
|
|
153
|
-
}
|
|
154
|
-
const typeAwareCfg = (
|
|
155
|
-
|
|
156
|
-
| { typeAware?: Record<string, unknown> }
|
|
157
|
-
| undefined
|
|
158
|
-
)?.typeAware;
|
|
202
|
+
let rawProjectConfig: Awaited<ReturnType<typeof readConfig>> | undefined;
|
|
203
|
+
try {
|
|
204
|
+
rawProjectConfig = projectConfig ?? await readConfig(projectRoot);
|
|
205
|
+
} catch {
|
|
206
|
+
rawProjectConfig = projectConfig ?? undefined;
|
|
207
|
+
}
|
|
208
|
+
const typeAwareCfg = (
|
|
209
|
+
rawProjectConfig?.guard as
|
|
210
|
+
| { typeAware?: Record<string, unknown> }
|
|
211
|
+
| undefined
|
|
212
|
+
)?.typeAware;
|
|
159
213
|
const typeAwareEnabled =
|
|
160
214
|
typeAwareArg !== undefined ? typeAwareArg : typeAwareCfg !== undefined;
|
|
161
215
|
|
|
@@ -175,21 +229,40 @@ export function guardTools(projectRoot: string) {
|
|
|
175
229
|
skipped: bridge.skipped,
|
|
176
230
|
summary: bridge.summary,
|
|
177
231
|
violations: bridge.violations,
|
|
178
|
-
};
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const typeAwareViolations = typeAwareResponse
|
|
236
|
+
? typeAwareResponse.violations as Array<{ severity: string }>
|
|
237
|
+
: [];
|
|
238
|
+
const typeAwareErrorCount = typeAwareViolations.filter(
|
|
239
|
+
(v) => v.severity === "error",
|
|
240
|
+
).length;
|
|
241
|
+
const legacyViolations = checkResult.violations.map(summarizeLegacyViolation);
|
|
242
|
+
const combinedViolations = [
|
|
243
|
+
...architectureViolations,
|
|
244
|
+
...legacyViolations,
|
|
245
|
+
];
|
|
246
|
+
const allPassed = checkResult.passed && architecturePassed && typeAwareErrorCount === 0;
|
|
247
|
+
const blockingViolationCount = combinedViolations.length + typeAwareErrorCount;
|
|
248
|
+
|
|
249
|
+
if (allPassed) {
|
|
250
|
+
return {
|
|
251
|
+
passed: true,
|
|
252
|
+
violations: [],
|
|
253
|
+
message: "All guard checks passed",
|
|
254
|
+
architecture: {
|
|
255
|
+
passed: true,
|
|
256
|
+
totalViolations: architectureReport.totalViolations,
|
|
257
|
+
bySeverity: architectureReport.bySeverity,
|
|
258
|
+
},
|
|
259
|
+
legacy: {
|
|
260
|
+
passed: true,
|
|
261
|
+
violations: 0,
|
|
262
|
+
},
|
|
263
|
+
relatedSkills: ["mandu-guard-guide", "mandu-debug"],
|
|
264
|
+
...(typeAwareResponse ? { typeAware: typeAwareResponse } : {}),
|
|
265
|
+
};
|
|
193
266
|
}
|
|
194
267
|
|
|
195
268
|
// If auto-correct requested and there are violations
|
|
@@ -200,33 +273,54 @@ export function guardTools(projectRoot: string) {
|
|
|
200
273
|
projectRoot
|
|
201
274
|
);
|
|
202
275
|
|
|
203
|
-
return {
|
|
204
|
-
passed:
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
276
|
+
return {
|
|
277
|
+
passed:
|
|
278
|
+
autoCorrectResult.fixed &&
|
|
279
|
+
architecturePassed &&
|
|
280
|
+
typeAwareErrorCount === 0,
|
|
281
|
+
violations: [
|
|
282
|
+
...architectureViolations,
|
|
283
|
+
...autoCorrectResult.remainingViolations.map(summarizeLegacyViolation),
|
|
284
|
+
],
|
|
285
|
+
autoCorrect: {
|
|
286
|
+
attempted: true,
|
|
287
|
+
fixed: autoCorrectResult.fixed,
|
|
209
288
|
steps: autoCorrectResult.steps,
|
|
210
289
|
retriedCount: autoCorrectResult.retriedCount,
|
|
211
290
|
rolledBack: autoCorrectResult.rolledBack,
|
|
212
|
-
changeId: autoCorrectResult.changeId,
|
|
213
|
-
},
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
291
|
+
changeId: autoCorrectResult.changeId,
|
|
292
|
+
},
|
|
293
|
+
architecture: {
|
|
294
|
+
passed: architecturePassed,
|
|
295
|
+
totalViolations: architectureReport.totalViolations,
|
|
296
|
+
bySeverity: architectureReport.bySeverity,
|
|
297
|
+
violations: architectureViolations,
|
|
298
|
+
},
|
|
299
|
+
legacy: {
|
|
300
|
+
passed: autoCorrectResult.fixed,
|
|
301
|
+
violations: autoCorrectResult.remainingViolations.length,
|
|
302
|
+
},
|
|
303
|
+
...(typeAwareResponse ? { typeAware: typeAwareResponse } : {}),
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return {
|
|
308
|
+
passed: false,
|
|
309
|
+
violations: combinedViolations,
|
|
310
|
+
message: `Found ${blockingViolationCount} violation(s)`,
|
|
311
|
+
architecture: {
|
|
312
|
+
passed: architecturePassed,
|
|
313
|
+
totalViolations: architectureReport.totalViolations,
|
|
314
|
+
bySeverity: architectureReport.bySeverity,
|
|
315
|
+
violations: architectureViolations,
|
|
316
|
+
},
|
|
317
|
+
legacy: {
|
|
318
|
+
passed: checkResult.passed,
|
|
319
|
+
violations: legacyViolations.length,
|
|
320
|
+
},
|
|
321
|
+
tip: "Use autoCorrect: true to attempt automatic fixes",
|
|
322
|
+
relatedSkills: ["mandu-guard-guide", "mandu-debug"],
|
|
323
|
+
...(typeAwareResponse ? { typeAware: typeAwareResponse } : {}),
|
|
230
324
|
};
|
|
231
325
|
},
|
|
232
326
|
|