@mandujs/core 0.54.11 → 0.54.13
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 +208 -200
- package/scripts/postinstall-lock.ts +153 -153
- package/src/a11y/run-audit.ts +15 -15
- package/src/agent/__tests__/context.test.ts +49 -1
- package/src/agent/context.ts +535 -535
- package/src/agent/index.ts +6 -6
- package/src/agent/plan.ts +282 -282
- package/src/agent/repair.ts +171 -171
- package/src/agent/sync.ts +200 -200
- package/src/agent/types.ts +8 -0
- package/src/agent/verify.ts +100 -2
- package/src/brain/doctor/analyzer.ts +7 -7
- package/src/bundler/__tests__/build-runner.ts +5 -4
- package/src/bundler/__tests__/cold-start.test.ts +60 -60
- package/src/bundler/__tests__/css.test.ts +20 -20
- package/src/bundler/analyzer.ts +15 -15
- package/src/bundler/build.test.ts +73 -7
- package/src/bundler/build.ts +139 -31
- package/src/bundler/css.ts +42 -42
- package/src/bundler/manifest-schema.ts +21 -21
- package/src/bundler/plugins/__tests__/block-generated-imports.test.ts +13 -13
- package/src/bundler/plugins/block-generated-imports.ts +13 -13
- package/src/bundler/types.ts +31 -31
- package/src/client/island.ts +79 -79
- package/src/config/validate.ts +1 -1
- package/src/contract/schema.ts +7 -0
- package/src/deploy/inference/context.ts +82 -82
- package/src/devtools/client/components/panel/islands-panel.tsx +16 -16
- package/src/devtools/client/components/panel/panel-container.tsx +1 -1
- package/src/error/formatter.ts +10 -1
- package/src/experimental/index.ts +10 -0
- package/src/filling/context.ts +17 -17
- package/src/filling/filling.ts +22 -1
- package/src/filling/index.ts +15 -1
- package/src/generator/generate.ts +30 -30
- package/src/generator/index.ts +3 -3
- package/src/generator/templates.ts +210 -210
- package/src/guard/check.ts +9 -9
- package/src/guard/config-guard.ts +13 -13
- package/src/guard/fs-routes-policy.ts +51 -51
- package/src/guard/index.ts +11 -11
- package/src/index.ts +0 -10
- package/src/internal/index.ts +25 -0
- package/src/kitchen/api/file-api.ts +11 -11
- package/src/report/index.ts +1 -1
- package/src/resource/__tests__/generator.test.ts +6 -6
- package/src/resource/__tests__/schema.test.ts +14 -14
- package/src/resource/ddl/__tests__/emit.test.ts +165 -165
- package/src/resource/ddl/emit.ts +146 -146
- package/src/resource/generator-schema.ts +11 -11
- package/src/resource/generators/slot.ts +72 -72
- package/src/resource/schema.ts +21 -21
- package/src/router/client-entry.test.ts +69 -33
- package/src/router/client-entry.ts +134 -74
- package/src/router/fs-routes.ts +24 -22
- package/src/router/fs-scanner.ts +21 -17
- package/src/router/fs-types.ts +8 -5
- package/src/runtime/__tests__/devtools-adapter.test.ts +68 -68
- package/src/runtime/__tests__/observability-lifecycle.test.ts +103 -103
- package/src/runtime/__tests__/page-render-response.test.ts +103 -103
- package/src/runtime/__tests__/request-middleware.test.ts +70 -70
- package/src/runtime/devtools-adapter.ts +68 -68
- package/src/runtime/escape.ts +34 -34
- package/src/runtime/image-feature.ts +15 -0
- package/src/runtime/observability-lifecycle.ts +290 -290
- package/src/runtime/page-render-response.ts +106 -106
- package/src/runtime/rate-limit.ts +231 -0
- package/src/runtime/request-middleware.ts +31 -31
- package/src/runtime/scheduler-lifecycle.ts +64 -0
- package/src/runtime/server.ts +27 -295
- package/src/runtime/ssr.ts +59 -59
- package/src/runtime/static-files.ts +289 -289
- package/src/runtime/streaming-ssr.ts +22 -22
- package/src/spec/schema.ts +4 -3
- package/src/watcher/__tests__/watcher.test.ts +59 -59
- package/src/watcher/watcher.ts +61 -61
package/src/agent/index.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
export * from "./types";
|
|
2
|
-
export * from "./context";
|
|
3
|
-
export * from "./verify";
|
|
4
|
-
export * from "./repair";
|
|
5
|
-
export * from "./plan";
|
|
6
|
-
export * from "./sync";
|
|
1
|
+
export * from "./types";
|
|
2
|
+
export * from "./context";
|
|
3
|
+
export * from "./verify";
|
|
4
|
+
export * from "./repair";
|
|
5
|
+
export * from "./plan";
|
|
6
|
+
export * from "./sync";
|
package/src/agent/plan.ts
CHANGED
|
@@ -1,282 +1,282 @@
|
|
|
1
|
-
import fs from "fs/promises";
|
|
2
|
-
import path from "path";
|
|
3
|
-
import type {
|
|
4
|
-
AgentApplyReport,
|
|
5
|
-
AgentDomain,
|
|
6
|
-
AgentPlan,
|
|
7
|
-
AgentPlanRisk,
|
|
8
|
-
AgentSuggestedCommand,
|
|
9
|
-
BuildAgentApplyOptions,
|
|
10
|
-
BuildAgentPlanOptions,
|
|
11
|
-
} from "./types";
|
|
12
|
-
|
|
13
|
-
const DEFAULT_PLAN_PATH = ".mandu/agent-plan.json";
|
|
14
|
-
|
|
15
|
-
const DOMAIN_KEYWORDS: Array<[AgentDomain, RegExp]> = [
|
|
16
|
-
["hydration", /\b(hydrat|island|partial|client|counter|interactive|ssr data)\b/i],
|
|
17
|
-
["contract", /\b(contract|openapi|schema|typed api|rpc)\b/i],
|
|
18
|
-
["api", /\b(api|endpoint|route handler|server action|post|get|put|delete)\b/i],
|
|
19
|
-
["slot", /\b(slot|filling|ctx\.|handler chain)\b/i],
|
|
20
|
-
["guard", /\b(guard|architecture|import boundary|layer|rule)\b/i],
|
|
21
|
-
["testing", /\b(test|spec|coverage|ate|e2e|playwright)\b/i],
|
|
22
|
-
["deploy", /\b(deploy|vercel|netlify|fly|render|docker|worker|edge)\b/i],
|
|
23
|
-
["design", /\b(design|ui|component|style|theme|tailwind|shadcn)\b/i],
|
|
24
|
-
["docs", /\b(doc|readme|guide|manual|changelog)\b/i],
|
|
25
|
-
["db", /\b(database|db|migration|seed|sqlite|postgres)\b/i],
|
|
26
|
-
["route", /\b(page|route|dashboard|screen|layout|fs route)\b/i],
|
|
27
|
-
];
|
|
28
|
-
|
|
29
|
-
function unique<T>(values: T[]): T[] {
|
|
30
|
-
return [...new Set(values)];
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function inferDomains(intent: string): AgentDomain[] {
|
|
34
|
-
const domains = DOMAIN_KEYWORDS
|
|
35
|
-
.filter(([, pattern]) => pattern.test(intent))
|
|
36
|
-
.map(([domain]) => domain);
|
|
37
|
-
|
|
38
|
-
if (domains.includes("api") && !domains.includes("contract")) {
|
|
39
|
-
domains.push("contract");
|
|
40
|
-
}
|
|
41
|
-
if (domains.includes("hydration") && !domains.includes("route")) {
|
|
42
|
-
domains.push("route");
|
|
43
|
-
}
|
|
44
|
-
if (domains.length === 0) {
|
|
45
|
-
domains.push("unknown");
|
|
46
|
-
}
|
|
47
|
-
return unique(domains);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function filesToRead(domains: AgentDomain[]): string[] {
|
|
51
|
-
const out: string[] = ["docs/plans/20_agent_surface_consolidation_plan.md"];
|
|
52
|
-
if (domains.includes("route")) out.push("app/", ".mandu/routes.manifest.json");
|
|
53
|
-
if (domains.includes("api")) out.push("app/api/", "spec/contracts/");
|
|
54
|
-
if (domains.includes("contract")) out.push("spec/contracts/", "docs/resource-architecture.md");
|
|
55
|
-
if (domains.includes("slot")) out.push("spec/slots/", "packages/mcp/src/resources/skills/mandu-slot/SKILL.md");
|
|
56
|
-
if (domains.includes("hydration")) out.push("packages/mcp/src/resources/skills/mandu-hydration/SKILL.md", "app/**/*.partial.tsx");
|
|
57
|
-
if (domains.includes("guard")) out.push("mandu.config.ts", "packages/mcp/src/resources/skills/mandu-guard/SKILL.md");
|
|
58
|
-
if (domains.includes("testing")) out.push("packages/mcp/src/resources/skills/mandu-testing/SKILL.md");
|
|
59
|
-
if (domains.includes("deploy")) out.push(".mandu/deploy.intent.json", "docs/deploy/README.md");
|
|
60
|
-
if (domains.includes("design")) out.push("DESIGN.md", "packages/mcp/src/resources/skills/mandu-ui/SKILL.md");
|
|
61
|
-
if (domains.includes("db")) out.push("spec/resources/", "spec/migrations/");
|
|
62
|
-
return unique(out);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function filesToCreate(intent: string, domains: AgentDomain[]): string[] {
|
|
66
|
-
const lower = intent.toLowerCase();
|
|
67
|
-
const out: string[] = [];
|
|
68
|
-
if (domains.includes("route")) {
|
|
69
|
-
if (lower.includes("dashboard")) out.push("app/dashboard/page.tsx");
|
|
70
|
-
else out.push("app/<route>/page.tsx");
|
|
71
|
-
}
|
|
72
|
-
if (domains.includes("api")) out.push("app/api/<name>/route.ts");
|
|
73
|
-
if (domains.includes("contract")) out.push("spec/contracts/<name>.contract.ts");
|
|
74
|
-
if (domains.includes("slot")) out.push("spec/slots/<name>.slot.ts");
|
|
75
|
-
if (domains.includes("hydration")) out.push("app/<route>/<component>.partial.tsx");
|
|
76
|
-
if (domains.includes("testing")) out.push("packages/<target>/__tests__/<feature>.test.ts");
|
|
77
|
-
if (domains.includes("docs")) out.push("docs/<topic>.md");
|
|
78
|
-
return unique(out);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function mcpTools(domains: AgentDomain[]): string[] {
|
|
82
|
-
const out = ["mandu.agent.context", "mandu.agent.verify"];
|
|
83
|
-
if (domains.includes("route")) out.push("mandu.route.list", "mandu.generate");
|
|
84
|
-
if (domains.includes("api")) out.push("mandu.generate", "mandu.contract.create");
|
|
85
|
-
if (domains.includes("contract")) out.push("mandu.contract.validate", "mandu.contract.openapi");
|
|
86
|
-
if (domains.includes("slot")) out.push("mandu.slot.validate", "mandu.slot.constraints");
|
|
87
|
-
if (domains.includes("hydration")) out.push("mandu.island.list", "mandu.hydration.set");
|
|
88
|
-
if (domains.includes("guard")) out.push("mandu.guard.check", "mandu.guard.explain");
|
|
89
|
-
if (domains.includes("testing")) out.push("mandu.run.tests", "mandu.ate.generate");
|
|
90
|
-
if (domains.includes("deploy")) out.push("mandu.deploy.plan", "mandu.deploy.compile");
|
|
91
|
-
if (domains.includes("design")) out.push("mandu.design.get", "mandu.design.check");
|
|
92
|
-
return unique(out);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function risks(domains: AgentDomain[]): AgentPlanRisk[] {
|
|
96
|
-
const out: AgentPlanRisk[] = [];
|
|
97
|
-
if (domains.includes("db")) out.push({ level: "high", reason: "Database changes can affect persistent data." });
|
|
98
|
-
if (domains.includes("deploy")) out.push({ level: "high", reason: "Deploy artifacts may affect production behavior." });
|
|
99
|
-
if (domains.includes("guard")) out.push({ level: "medium", reason: "Architecture rule changes can hide future violations." });
|
|
100
|
-
if (domains.includes("hydration")) out.push({ level: "medium", reason: "SSR/client boundaries can regress hydration." });
|
|
101
|
-
if (domains.includes("api") || domains.includes("contract")) {
|
|
102
|
-
out.push({ level: "medium", reason: "API and contract changes must stay synchronized." });
|
|
103
|
-
}
|
|
104
|
-
if (out.length === 0) out.push({ level: "low", reason: "No high-risk domain detected by deterministic planner." });
|
|
105
|
-
return out;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
function verification(domains: AgentDomain[]): AgentSuggestedCommand[] {
|
|
109
|
-
const out: AgentSuggestedCommand[] = [
|
|
110
|
-
{
|
|
111
|
-
command: "mandu agent verify --changed --json --write",
|
|
112
|
-
reason: "Canonical post-change verification gate.",
|
|
113
|
-
required: true,
|
|
114
|
-
},
|
|
115
|
-
{
|
|
116
|
-
command: "bun run typecheck",
|
|
117
|
-
reason: "Type boundary check for generated or edited TypeScript.",
|
|
118
|
-
required: true,
|
|
119
|
-
},
|
|
120
|
-
];
|
|
121
|
-
if (domains.includes("hydration") || domains.includes("route")) {
|
|
122
|
-
out.push({
|
|
123
|
-
command: "bun test packages/core/src/bundler packages/core/tests/client",
|
|
124
|
-
reason: "Route and hydration changes can affect bundling and client runtime.",
|
|
125
|
-
required: true,
|
|
126
|
-
});
|
|
127
|
-
}
|
|
128
|
-
if (domains.includes("api") || domains.includes("contract") || domains.includes("slot")) {
|
|
129
|
-
out.push({
|
|
130
|
-
command: "bun test packages/core/src/contract packages/core/src/slot packages/core/tests/routes",
|
|
131
|
-
reason: "API, contract, and slot changes need framework contract coverage.",
|
|
132
|
-
required: true,
|
|
133
|
-
});
|
|
134
|
-
}
|
|
135
|
-
if (domains.includes("deploy")) {
|
|
136
|
-
out.push({
|
|
137
|
-
command: "mandu deploy:plan --dry-run",
|
|
138
|
-
reason: "Deploy intent should be inspected before writing provider artifacts.",
|
|
139
|
-
required: true,
|
|
140
|
-
});
|
|
141
|
-
}
|
|
142
|
-
return unique(out.map((item) => JSON.stringify(item))).map((item) => JSON.parse(item) as AgentSuggestedCommand);
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
export function buildAgentPlan(options: BuildAgentPlanOptions): AgentPlan {
|
|
146
|
-
const intent = options.intent.trim();
|
|
147
|
-
const domains = inferDomains(intent);
|
|
148
|
-
return {
|
|
149
|
-
schemaVersion: 1,
|
|
150
|
-
framework: "mandu",
|
|
151
|
-
generatedAt: new Date().toISOString(),
|
|
152
|
-
intent,
|
|
153
|
-
domains,
|
|
154
|
-
filesToRead: filesToRead(domains),
|
|
155
|
-
filesToCreate: filesToCreate(intent, domains),
|
|
156
|
-
filesToModify: [],
|
|
157
|
-
mcpTools: mcpTools(domains),
|
|
158
|
-
risks: risks(domains),
|
|
159
|
-
verification: verification(domains),
|
|
160
|
-
notes: [
|
|
161
|
-
"This deterministic plan is intentionally conservative. It does not execute edits.",
|
|
162
|
-
"Prefer MCP/domain tools before direct file edits, then run agent verify.",
|
|
163
|
-
],
|
|
164
|
-
executable: false,
|
|
165
|
-
};
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
export function agentPlanPath(rootDir: string): string {
|
|
169
|
-
return path.join(rootDir, ".mandu", "agent-plan.json");
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
export async function writeAgentPlan(rootDir: string, plan: AgentPlan): Promise<{ path: string; plan: AgentPlan }> {
|
|
173
|
-
const outPath = agentPlanPath(rootDir);
|
|
174
|
-
await fs.mkdir(path.dirname(outPath), { recursive: true });
|
|
175
|
-
await fs.writeFile(outPath, `${JSON.stringify(plan, null, 2)}\n`, "utf8");
|
|
176
|
-
return { path: outPath, plan };
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
async function readAgentPlan(filePath: string): Promise<AgentPlan | null> {
|
|
180
|
-
try {
|
|
181
|
-
const raw = await fs.readFile(filePath, "utf8");
|
|
182
|
-
const parsed = JSON.parse(raw) as AgentPlan;
|
|
183
|
-
if (parsed?.framework !== "mandu" || !Array.isArray(parsed.domains)) {
|
|
184
|
-
return null;
|
|
185
|
-
}
|
|
186
|
-
return parsed;
|
|
187
|
-
} catch {
|
|
188
|
-
return null;
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
function resolveInside(rootDir: string, value: string): string {
|
|
193
|
-
const root = path.resolve(rootDir);
|
|
194
|
-
const resolved = path.resolve(root, value);
|
|
195
|
-
if (resolved !== root && !resolved.startsWith(root + path.sep)) {
|
|
196
|
-
throw new Error("agent apply input must stay inside the project root");
|
|
197
|
-
}
|
|
198
|
-
return resolved;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
export async function buildAgentApplyReport(
|
|
202
|
-
rootDir: string = process.cwd(),
|
|
203
|
-
options: BuildAgentApplyOptions = {},
|
|
204
|
-
): Promise<AgentApplyReport> {
|
|
205
|
-
const sourceRel = options.from ?? DEFAULT_PLAN_PATH;
|
|
206
|
-
const sourcePath = resolveInside(rootDir, sourceRel);
|
|
207
|
-
const plan = await readAgentPlan(sourcePath);
|
|
208
|
-
if (!plan) {
|
|
209
|
-
return {
|
|
210
|
-
schemaVersion: 1,
|
|
211
|
-
framework: "mandu",
|
|
212
|
-
generatedAt: new Date().toISOString(),
|
|
213
|
-
ok: false,
|
|
214
|
-
dryRun: true,
|
|
215
|
-
sourcePlan: sourceRel,
|
|
216
|
-
intent: "",
|
|
217
|
-
domains: ["unknown"],
|
|
218
|
-
actions: [
|
|
219
|
-
{
|
|
220
|
-
kind: "verify",
|
|
221
|
-
description: "Create an agent plan first.",
|
|
222
|
-
command: "mandu agent plan \"<task>\" --json --write",
|
|
223
|
-
applied: false,
|
|
224
|
-
},
|
|
225
|
-
],
|
|
226
|
-
warnings: [`Could not read ${sourceRel}.`],
|
|
227
|
-
nextVerifyCommand: "mandu agent verify --changed --json --write",
|
|
228
|
-
};
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
return {
|
|
232
|
-
schemaVersion: 1,
|
|
233
|
-
framework: "mandu",
|
|
234
|
-
generatedAt: new Date().toISOString(),
|
|
235
|
-
ok: true,
|
|
236
|
-
dryRun: options.dryRun !== false,
|
|
237
|
-
sourcePlan: sourceRel,
|
|
238
|
-
intent: plan.intent,
|
|
239
|
-
domains: plan.domains,
|
|
240
|
-
actions: [
|
|
241
|
-
...plan.filesToRead.map((file) => ({
|
|
242
|
-
kind: "read_file" as const,
|
|
243
|
-
description: `Read ${file} before editing.`,
|
|
244
|
-
file,
|
|
245
|
-
applied: false as const,
|
|
246
|
-
})),
|
|
247
|
-
...plan.mcpTools.map((tool) => ({
|
|
248
|
-
kind: "mcp_tool" as const,
|
|
249
|
-
description: `Consider ${tool} for this plan.`,
|
|
250
|
-
tool,
|
|
251
|
-
applied: false as const,
|
|
252
|
-
})),
|
|
253
|
-
...plan.filesToCreate.map((file) => ({
|
|
254
|
-
kind: "manual_edit" as const,
|
|
255
|
-
description: `Create ${file} only after confirming the local pattern.`,
|
|
256
|
-
file,
|
|
257
|
-
applied: false as const,
|
|
258
|
-
})),
|
|
259
|
-
...plan.verification.map((item) => ({
|
|
260
|
-
kind: "verify" as const,
|
|
261
|
-
description: item.reason,
|
|
262
|
-
command: item.command,
|
|
263
|
-
applied: false as const,
|
|
264
|
-
})),
|
|
265
|
-
],
|
|
266
|
-
warnings: [
|
|
267
|
-
"Agent apply is dry-run only until typed operation payloads are introduced.",
|
|
268
|
-
"No filesystem changes were made by this report.",
|
|
269
|
-
],
|
|
270
|
-
nextVerifyCommand: "mandu agent verify --changed --json --write",
|
|
271
|
-
};
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
export async function writeAgentApplyReport(
|
|
275
|
-
rootDir: string,
|
|
276
|
-
report: AgentApplyReport,
|
|
277
|
-
): Promise<{ path: string; report: AgentApplyReport }> {
|
|
278
|
-
const outPath = path.join(rootDir, ".mandu", "agent-apply.json");
|
|
279
|
-
await fs.mkdir(path.dirname(outPath), { recursive: true });
|
|
280
|
-
await fs.writeFile(outPath, `${JSON.stringify(report, null, 2)}\n`, "utf8");
|
|
281
|
-
return { path: outPath, report };
|
|
282
|
-
}
|
|
1
|
+
import fs from "fs/promises";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import type {
|
|
4
|
+
AgentApplyReport,
|
|
5
|
+
AgentDomain,
|
|
6
|
+
AgentPlan,
|
|
7
|
+
AgentPlanRisk,
|
|
8
|
+
AgentSuggestedCommand,
|
|
9
|
+
BuildAgentApplyOptions,
|
|
10
|
+
BuildAgentPlanOptions,
|
|
11
|
+
} from "./types";
|
|
12
|
+
|
|
13
|
+
const DEFAULT_PLAN_PATH = ".mandu/agent-plan.json";
|
|
14
|
+
|
|
15
|
+
const DOMAIN_KEYWORDS: Array<[AgentDomain, RegExp]> = [
|
|
16
|
+
["hydration", /\b(hydrat|island|partial|client|counter|interactive|ssr data)\b/i],
|
|
17
|
+
["contract", /\b(contract|openapi|schema|typed api|rpc)\b/i],
|
|
18
|
+
["api", /\b(api|endpoint|route handler|server action|post|get|put|delete)\b/i],
|
|
19
|
+
["slot", /\b(slot|filling|ctx\.|handler chain)\b/i],
|
|
20
|
+
["guard", /\b(guard|architecture|import boundary|layer|rule)\b/i],
|
|
21
|
+
["testing", /\b(test|spec|coverage|ate|e2e|playwright)\b/i],
|
|
22
|
+
["deploy", /\b(deploy|vercel|netlify|fly|render|docker|worker|edge)\b/i],
|
|
23
|
+
["design", /\b(design|ui|component|style|theme|tailwind|shadcn)\b/i],
|
|
24
|
+
["docs", /\b(doc|readme|guide|manual|changelog)\b/i],
|
|
25
|
+
["db", /\b(database|db|migration|seed|sqlite|postgres)\b/i],
|
|
26
|
+
["route", /\b(page|route|dashboard|screen|layout|fs route)\b/i],
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
function unique<T>(values: T[]): T[] {
|
|
30
|
+
return [...new Set(values)];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function inferDomains(intent: string): AgentDomain[] {
|
|
34
|
+
const domains = DOMAIN_KEYWORDS
|
|
35
|
+
.filter(([, pattern]) => pattern.test(intent))
|
|
36
|
+
.map(([domain]) => domain);
|
|
37
|
+
|
|
38
|
+
if (domains.includes("api") && !domains.includes("contract")) {
|
|
39
|
+
domains.push("contract");
|
|
40
|
+
}
|
|
41
|
+
if (domains.includes("hydration") && !domains.includes("route")) {
|
|
42
|
+
domains.push("route");
|
|
43
|
+
}
|
|
44
|
+
if (domains.length === 0) {
|
|
45
|
+
domains.push("unknown");
|
|
46
|
+
}
|
|
47
|
+
return unique(domains);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function filesToRead(domains: AgentDomain[]): string[] {
|
|
51
|
+
const out: string[] = ["docs/plans/20_agent_surface_consolidation_plan.md"];
|
|
52
|
+
if (domains.includes("route")) out.push("app/", ".mandu/routes.manifest.json");
|
|
53
|
+
if (domains.includes("api")) out.push("app/api/", "spec/contracts/");
|
|
54
|
+
if (domains.includes("contract")) out.push("spec/contracts/", "docs/resource-architecture.md");
|
|
55
|
+
if (domains.includes("slot")) out.push("spec/slots/", "packages/mcp/src/resources/skills/mandu-slot/SKILL.md");
|
|
56
|
+
if (domains.includes("hydration")) out.push("packages/mcp/src/resources/skills/mandu-hydration/SKILL.md", "app/**/*.partial.tsx");
|
|
57
|
+
if (domains.includes("guard")) out.push("mandu.config.ts", "packages/mcp/src/resources/skills/mandu-guard/SKILL.md");
|
|
58
|
+
if (domains.includes("testing")) out.push("packages/mcp/src/resources/skills/mandu-testing/SKILL.md");
|
|
59
|
+
if (domains.includes("deploy")) out.push(".mandu/deploy.intent.json", "docs/deploy/README.md");
|
|
60
|
+
if (domains.includes("design")) out.push("DESIGN.md", "packages/mcp/src/resources/skills/mandu-ui/SKILL.md");
|
|
61
|
+
if (domains.includes("db")) out.push("spec/resources/", "spec/migrations/");
|
|
62
|
+
return unique(out);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function filesToCreate(intent: string, domains: AgentDomain[]): string[] {
|
|
66
|
+
const lower = intent.toLowerCase();
|
|
67
|
+
const out: string[] = [];
|
|
68
|
+
if (domains.includes("route")) {
|
|
69
|
+
if (lower.includes("dashboard")) out.push("app/dashboard/page.tsx");
|
|
70
|
+
else out.push("app/<route>/page.tsx");
|
|
71
|
+
}
|
|
72
|
+
if (domains.includes("api")) out.push("app/api/<name>/route.ts");
|
|
73
|
+
if (domains.includes("contract")) out.push("spec/contracts/<name>.contract.ts");
|
|
74
|
+
if (domains.includes("slot")) out.push("spec/slots/<name>.slot.ts");
|
|
75
|
+
if (domains.includes("hydration")) out.push("app/<route>/<component>.partial.tsx");
|
|
76
|
+
if (domains.includes("testing")) out.push("packages/<target>/__tests__/<feature>.test.ts");
|
|
77
|
+
if (domains.includes("docs")) out.push("docs/<topic>.md");
|
|
78
|
+
return unique(out);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function mcpTools(domains: AgentDomain[]): string[] {
|
|
82
|
+
const out = ["mandu.agent.context", "mandu.agent.verify"];
|
|
83
|
+
if (domains.includes("route")) out.push("mandu.route.list", "mandu.generate");
|
|
84
|
+
if (domains.includes("api")) out.push("mandu.generate", "mandu.contract.create");
|
|
85
|
+
if (domains.includes("contract")) out.push("mandu.contract.validate", "mandu.contract.openapi");
|
|
86
|
+
if (domains.includes("slot")) out.push("mandu.slot.validate", "mandu.slot.constraints");
|
|
87
|
+
if (domains.includes("hydration")) out.push("mandu.island.list", "mandu.hydration.set");
|
|
88
|
+
if (domains.includes("guard")) out.push("mandu.guard.check", "mandu.guard.explain");
|
|
89
|
+
if (domains.includes("testing")) out.push("mandu.run.tests", "mandu.ate.generate");
|
|
90
|
+
if (domains.includes("deploy")) out.push("mandu.deploy.plan", "mandu.deploy.compile");
|
|
91
|
+
if (domains.includes("design")) out.push("mandu.design.get", "mandu.design.check");
|
|
92
|
+
return unique(out);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function risks(domains: AgentDomain[]): AgentPlanRisk[] {
|
|
96
|
+
const out: AgentPlanRisk[] = [];
|
|
97
|
+
if (domains.includes("db")) out.push({ level: "high", reason: "Database changes can affect persistent data." });
|
|
98
|
+
if (domains.includes("deploy")) out.push({ level: "high", reason: "Deploy artifacts may affect production behavior." });
|
|
99
|
+
if (domains.includes("guard")) out.push({ level: "medium", reason: "Architecture rule changes can hide future violations." });
|
|
100
|
+
if (domains.includes("hydration")) out.push({ level: "medium", reason: "SSR/client boundaries can regress hydration." });
|
|
101
|
+
if (domains.includes("api") || domains.includes("contract")) {
|
|
102
|
+
out.push({ level: "medium", reason: "API and contract changes must stay synchronized." });
|
|
103
|
+
}
|
|
104
|
+
if (out.length === 0) out.push({ level: "low", reason: "No high-risk domain detected by deterministic planner." });
|
|
105
|
+
return out;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function verification(domains: AgentDomain[]): AgentSuggestedCommand[] {
|
|
109
|
+
const out: AgentSuggestedCommand[] = [
|
|
110
|
+
{
|
|
111
|
+
command: "mandu agent verify --changed --json --write",
|
|
112
|
+
reason: "Canonical post-change verification gate.",
|
|
113
|
+
required: true,
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
command: "bun run typecheck",
|
|
117
|
+
reason: "Type boundary check for generated or edited TypeScript.",
|
|
118
|
+
required: true,
|
|
119
|
+
},
|
|
120
|
+
];
|
|
121
|
+
if (domains.includes("hydration") || domains.includes("route")) {
|
|
122
|
+
out.push({
|
|
123
|
+
command: "bun test packages/core/src/bundler packages/core/tests/client",
|
|
124
|
+
reason: "Route and hydration changes can affect bundling and client runtime.",
|
|
125
|
+
required: true,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
if (domains.includes("api") || domains.includes("contract") || domains.includes("slot")) {
|
|
129
|
+
out.push({
|
|
130
|
+
command: "bun test packages/core/src/contract packages/core/src/slot packages/core/tests/routes",
|
|
131
|
+
reason: "API, contract, and slot changes need framework contract coverage.",
|
|
132
|
+
required: true,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
if (domains.includes("deploy")) {
|
|
136
|
+
out.push({
|
|
137
|
+
command: "mandu deploy:plan --dry-run",
|
|
138
|
+
reason: "Deploy intent should be inspected before writing provider artifacts.",
|
|
139
|
+
required: true,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
return unique(out.map((item) => JSON.stringify(item))).map((item) => JSON.parse(item) as AgentSuggestedCommand);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function buildAgentPlan(options: BuildAgentPlanOptions): AgentPlan {
|
|
146
|
+
const intent = options.intent.trim();
|
|
147
|
+
const domains = inferDomains(intent);
|
|
148
|
+
return {
|
|
149
|
+
schemaVersion: 1,
|
|
150
|
+
framework: "mandu",
|
|
151
|
+
generatedAt: new Date().toISOString(),
|
|
152
|
+
intent,
|
|
153
|
+
domains,
|
|
154
|
+
filesToRead: filesToRead(domains),
|
|
155
|
+
filesToCreate: filesToCreate(intent, domains),
|
|
156
|
+
filesToModify: [],
|
|
157
|
+
mcpTools: mcpTools(domains),
|
|
158
|
+
risks: risks(domains),
|
|
159
|
+
verification: verification(domains),
|
|
160
|
+
notes: [
|
|
161
|
+
"This deterministic plan is intentionally conservative. It does not execute edits.",
|
|
162
|
+
"Prefer MCP/domain tools before direct file edits, then run agent verify.",
|
|
163
|
+
],
|
|
164
|
+
executable: false,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function agentPlanPath(rootDir: string): string {
|
|
169
|
+
return path.join(rootDir, ".mandu", "agent-plan.json");
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export async function writeAgentPlan(rootDir: string, plan: AgentPlan): Promise<{ path: string; plan: AgentPlan }> {
|
|
173
|
+
const outPath = agentPlanPath(rootDir);
|
|
174
|
+
await fs.mkdir(path.dirname(outPath), { recursive: true });
|
|
175
|
+
await fs.writeFile(outPath, `${JSON.stringify(plan, null, 2)}\n`, "utf8");
|
|
176
|
+
return { path: outPath, plan };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async function readAgentPlan(filePath: string): Promise<AgentPlan | null> {
|
|
180
|
+
try {
|
|
181
|
+
const raw = await fs.readFile(filePath, "utf8");
|
|
182
|
+
const parsed = JSON.parse(raw) as AgentPlan;
|
|
183
|
+
if (parsed?.framework !== "mandu" || !Array.isArray(parsed.domains)) {
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
return parsed;
|
|
187
|
+
} catch {
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function resolveInside(rootDir: string, value: string): string {
|
|
193
|
+
const root = path.resolve(rootDir);
|
|
194
|
+
const resolved = path.resolve(root, value);
|
|
195
|
+
if (resolved !== root && !resolved.startsWith(root + path.sep)) {
|
|
196
|
+
throw new Error("agent apply input must stay inside the project root");
|
|
197
|
+
}
|
|
198
|
+
return resolved;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export async function buildAgentApplyReport(
|
|
202
|
+
rootDir: string = process.cwd(),
|
|
203
|
+
options: BuildAgentApplyOptions = {},
|
|
204
|
+
): Promise<AgentApplyReport> {
|
|
205
|
+
const sourceRel = options.from ?? DEFAULT_PLAN_PATH;
|
|
206
|
+
const sourcePath = resolveInside(rootDir, sourceRel);
|
|
207
|
+
const plan = await readAgentPlan(sourcePath);
|
|
208
|
+
if (!plan) {
|
|
209
|
+
return {
|
|
210
|
+
schemaVersion: 1,
|
|
211
|
+
framework: "mandu",
|
|
212
|
+
generatedAt: new Date().toISOString(),
|
|
213
|
+
ok: false,
|
|
214
|
+
dryRun: true,
|
|
215
|
+
sourcePlan: sourceRel,
|
|
216
|
+
intent: "",
|
|
217
|
+
domains: ["unknown"],
|
|
218
|
+
actions: [
|
|
219
|
+
{
|
|
220
|
+
kind: "verify",
|
|
221
|
+
description: "Create an agent plan first.",
|
|
222
|
+
command: "mandu agent plan \"<task>\" --json --write",
|
|
223
|
+
applied: false,
|
|
224
|
+
},
|
|
225
|
+
],
|
|
226
|
+
warnings: [`Could not read ${sourceRel}.`],
|
|
227
|
+
nextVerifyCommand: "mandu agent verify --changed --json --write",
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return {
|
|
232
|
+
schemaVersion: 1,
|
|
233
|
+
framework: "mandu",
|
|
234
|
+
generatedAt: new Date().toISOString(),
|
|
235
|
+
ok: true,
|
|
236
|
+
dryRun: options.dryRun !== false,
|
|
237
|
+
sourcePlan: sourceRel,
|
|
238
|
+
intent: plan.intent,
|
|
239
|
+
domains: plan.domains,
|
|
240
|
+
actions: [
|
|
241
|
+
...plan.filesToRead.map((file) => ({
|
|
242
|
+
kind: "read_file" as const,
|
|
243
|
+
description: `Read ${file} before editing.`,
|
|
244
|
+
file,
|
|
245
|
+
applied: false as const,
|
|
246
|
+
})),
|
|
247
|
+
...plan.mcpTools.map((tool) => ({
|
|
248
|
+
kind: "mcp_tool" as const,
|
|
249
|
+
description: `Consider ${tool} for this plan.`,
|
|
250
|
+
tool,
|
|
251
|
+
applied: false as const,
|
|
252
|
+
})),
|
|
253
|
+
...plan.filesToCreate.map((file) => ({
|
|
254
|
+
kind: "manual_edit" as const,
|
|
255
|
+
description: `Create ${file} only after confirming the local pattern.`,
|
|
256
|
+
file,
|
|
257
|
+
applied: false as const,
|
|
258
|
+
})),
|
|
259
|
+
...plan.verification.map((item) => ({
|
|
260
|
+
kind: "verify" as const,
|
|
261
|
+
description: item.reason,
|
|
262
|
+
command: item.command,
|
|
263
|
+
applied: false as const,
|
|
264
|
+
})),
|
|
265
|
+
],
|
|
266
|
+
warnings: [
|
|
267
|
+
"Agent apply is dry-run only until typed operation payloads are introduced.",
|
|
268
|
+
"No filesystem changes were made by this report.",
|
|
269
|
+
],
|
|
270
|
+
nextVerifyCommand: "mandu agent verify --changed --json --write",
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export async function writeAgentApplyReport(
|
|
275
|
+
rootDir: string,
|
|
276
|
+
report: AgentApplyReport,
|
|
277
|
+
): Promise<{ path: string; report: AgentApplyReport }> {
|
|
278
|
+
const outPath = path.join(rootDir, ".mandu", "agent-apply.json");
|
|
279
|
+
await fs.mkdir(path.dirname(outPath), { recursive: true });
|
|
280
|
+
await fs.writeFile(outPath, `${JSON.stringify(report, null, 2)}\n`, "utf8");
|
|
281
|
+
return { path: outPath, report };
|
|
282
|
+
}
|