@mandujs/core 0.54.3 → 0.54.4

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.54.3",
3
+ "version": "0.54.4",
4
4
  "description": "Mandu Framework Core - Spec, Generator, Guard, Runtime",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -8,6 +8,7 @@
8
8
  "exports": {
9
9
  ".": "./src/index.ts",
10
10
  "./a11y": "./src/a11y/index.ts",
11
+ "./agent": "./src/agent/index.ts",
11
12
  "./auth": "./src/auth/index.ts",
12
13
  "./auth/login": "./src/auth/login.ts",
13
14
  "./auth/password": "./src/auth/password.ts",
@@ -0,0 +1,237 @@
1
+ import { afterEach, beforeEach, describe, expect, it } from "bun:test";
2
+ import fs from "fs/promises";
3
+ import os from "os";
4
+ import path from "path";
5
+ import {
6
+ agentManifestPath,
7
+ buildAgentContext,
8
+ readAgentManifest,
9
+ writeAgentManifest,
10
+ } from "../context";
11
+ import {
12
+ agentPlanPath,
13
+ buildAgentApplyReport,
14
+ buildAgentPlan,
15
+ writeAgentApplyReport,
16
+ writeAgentPlan,
17
+ } from "../plan";
18
+ import {
19
+ agentRepairReportPath,
20
+ buildAgentRepairReport,
21
+ writeAgentRepairReport,
22
+ } from "../repair";
23
+ import { buildAgentSyncReport } from "../sync";
24
+ import {
25
+ agentVerifyReportPath,
26
+ buildAgentVerifyReport,
27
+ writeAgentVerifyReport,
28
+ } from "../verify";
29
+
30
+ async function writeFile(root: string, rel: string, content: string): Promise<void> {
31
+ const abs = path.join(root, rel);
32
+ await fs.mkdir(path.dirname(abs), { recursive: true });
33
+ await fs.writeFile(abs, content, "utf8");
34
+ }
35
+
36
+ describe("agent context", () => {
37
+ let root: string;
38
+
39
+ beforeEach(async () => {
40
+ root = await fs.mkdtemp(path.join(os.tmpdir(), "mandu-agent-context-"));
41
+ await writeFile(root, "package.json", JSON.stringify({
42
+ name: "agent-app",
43
+ version: "1.2.3",
44
+ packageManager: "bun@1.3.14",
45
+ }));
46
+ await writeFile(root, ".mandu/routes.manifest.json", JSON.stringify({
47
+ version: 1,
48
+ routes: [
49
+ {
50
+ id: "home",
51
+ pattern: "/",
52
+ kind: "page",
53
+ module: "app/page.tsx",
54
+ componentModule: "app/page.tsx",
55
+ layoutChain: [],
56
+ },
57
+ {
58
+ id: "api-ping",
59
+ pattern: "/api/ping",
60
+ kind: "api",
61
+ module: "app/api/ping/route.ts",
62
+ methods: ["GET"],
63
+ contractModule: "spec/contracts/ping.contract.ts",
64
+ },
65
+ ],
66
+ }));
67
+ await writeFile(root, "app/dashboard/counter.partial.tsx", "export default function Counter() { return null; }");
68
+ await writeFile(root, "app/dashboard/chart.island.tsx", "export default function Chart() { return null; }");
69
+ await writeFile(root, "spec/slots/dashboard.slot.ts", "export default {};");
70
+ await writeFile(root, "spec/contracts/ping.contract.ts", "export default {};");
71
+ await writeFile(root, ".env.example", "DATABASE_URL=\n");
72
+ });
73
+
74
+ afterEach(async () => {
75
+ await fs.rm(root, { recursive: true, force: true });
76
+ });
77
+
78
+ it("builds a single agent-facing project map", async () => {
79
+ const context = await buildAgentContext(root, {
80
+ includeDiagnose: false,
81
+ includeGit: false,
82
+ });
83
+
84
+ expect(context.framework).toBe("mandu");
85
+ expect(context.project.name).toBe("agent-app");
86
+ expect(context.project.packageManager).toBe("bun@1.3.14");
87
+ expect(context.routeSource).toBe("manifest");
88
+ expect(context.routes).toHaveLength(2);
89
+ expect(context.pages.map((r) => r.id)).toEqual(["home"]);
90
+ expect(context.apis.map((r) => r.id)).toEqual(["api-ping"]);
91
+ expect(context.apis[0]?.hasContractModule).toBe(true);
92
+ expect(context.partials.map((p) => p.path)).toEqual(["app/dashboard/counter.partial.tsx"]);
93
+ expect(context.islands.map((p) => p.path)).toEqual(["app/dashboard/chart.island.tsx"]);
94
+ expect(context.slots.map((p) => p.path)).toEqual(["spec/slots/dashboard.slot.ts"]);
95
+ expect(context.contracts.map((p) => p.path)).toEqual(["spec/contracts/ping.contract.ts"]);
96
+ expect(context.env[0]).toMatchObject({ path: ".env.example", kind: "template", redacted: true });
97
+ expect(context.commands.context).toBe("mandu agent context --json");
98
+ expect(context.agentWorkflow.canonical).toEqual(["context", "plan", "apply", "verify", "repair"]);
99
+ });
100
+
101
+ it("writes and reads .mandu/agent-manifest.json", async () => {
102
+ const context = await buildAgentContext(root, {
103
+ includeDiagnose: false,
104
+ includeGit: false,
105
+ });
106
+ const result = await writeAgentManifest(root, context);
107
+
108
+ expect(result.path).toBe(agentManifestPath(root));
109
+ const manifest = await readAgentManifest(root);
110
+ expect(manifest?.schemaVersion).toBe(1);
111
+ expect(manifest?.project.name).toBe("agent-app");
112
+ expect(manifest?.agentWorkflow.canonical).toEqual(["context", "plan", "apply", "verify", "repair"]);
113
+ });
114
+
115
+ it("builds a deterministic agent plan from intent", () => {
116
+ const plan = buildAgentPlan({
117
+ intent: "add dashboard page with API contract and partial island",
118
+ });
119
+
120
+ expect(plan.framework).toBe("mandu");
121
+ expect(plan.intent).toBe("add dashboard page with API contract and partial island");
122
+ expect(plan.domains).toContain("route");
123
+ expect(plan.domains).toContain("api");
124
+ expect(plan.domains).toContain("contract");
125
+ expect(plan.domains).toContain("hydration");
126
+ expect(plan.filesToCreate).toContain("app/dashboard/page.tsx");
127
+ expect(plan.filesToCreate).toContain("app/api/<name>/route.ts");
128
+ expect(plan.mcpTools).toContain("mandu.agent.verify");
129
+ expect(plan.mcpTools).toContain("mandu.contract.validate");
130
+ expect(plan.verification.map((cmd) => cmd.command)).toContain("bun run typecheck");
131
+ });
132
+
133
+ it("writes a plan and previews apply actions without mutating files", async () => {
134
+ const plan = buildAgentPlan({
135
+ intent: "add dashboard page with API contract and partial island",
136
+ });
137
+ const written = await writeAgentPlan(root, plan);
138
+ expect(written.path).toBe(agentPlanPath(root));
139
+
140
+ const report = await buildAgentApplyReport(root);
141
+ expect(report.ok).toBe(true);
142
+ expect(report.dryRun).toBe(true);
143
+ expect(report.intent).toBe(plan.intent);
144
+ expect(report.actions.some((action) => action.kind === "mcp_tool")).toBe(true);
145
+ expect(report.actions.some((action) => action.kind === "manual_edit")).toBe(true);
146
+ expect(report.actions.every((action) => action.applied === false)).toBe(true);
147
+
148
+ const result = await writeAgentApplyReport(root, report);
149
+ expect(result.path).toBe(path.join(root, ".mandu", "agent-apply.json"));
150
+ expect(JSON.parse(await fs.readFile(result.path, "utf8")).intent).toBe(plan.intent);
151
+ });
152
+
153
+ it("syncs agent workflow artifacts for Codex, Claude, and Gemini", async () => {
154
+ const report = await buildAgentSyncReport(root, { target: "all" });
155
+
156
+ expect(report.ok).toBe(true);
157
+ expect(report.profile).toBe("agent-core");
158
+ expect(report.workflow).toEqual(["context", "plan", "apply", "verify", "repair"]);
159
+ expect(report.files.map((file) => file.path)).toContain(".mandu/agent-sync/codex/AGENTS.md");
160
+ expect(report.files.map((file) => file.path)).toContain(".mandu/agent-sync/claude/CLAUDE.md");
161
+ expect(report.files.map((file) => file.path)).toContain(".mandu/agent-sync/gemini/GEMINI.md");
162
+ const codex = await fs.readFile(path.join(root, ".mandu", "agent-sync", "codex", "AGENTS.md"), "utf8");
163
+ expect(codex).toContain("context -> plan -> apply -> verify -> repair");
164
+ expect(codex).toContain("MANDU_MCP_PROFILE=agent-core");
165
+ });
166
+
167
+ it("builds and writes the agent verify report", async () => {
168
+ const report = await buildAgentVerifyReport(root, {
169
+ includeDiagnose: false,
170
+ includeGit: false,
171
+ includeGuard: false,
172
+ includeContract: false,
173
+ });
174
+
175
+ expect(report.framework).toBe("mandu");
176
+ expect(report.ok).toBe(true);
177
+ expect(report.checks.map((check) => check.id)).toEqual(["manifest"]);
178
+ expect(report.suggestedCommands.map((cmd) => cmd.command)).toContain("bun run typecheck");
179
+ expect(report.nextRepairInput).toBe(".mandu/agent-verify.json");
180
+
181
+ const result = await writeAgentVerifyReport(root, report);
182
+ expect(result.path).toBe(agentVerifyReportPath(root));
183
+ const parsed = JSON.parse(await fs.readFile(result.path, "utf8"));
184
+ expect(parsed.project.name).toBe("agent-app");
185
+ });
186
+
187
+ it("turns a verify report into repair actions", async () => {
188
+ await writeAgentVerifyReport(root, {
189
+ schemaVersion: 1,
190
+ framework: "mandu",
191
+ generatedAt: new Date().toISOString(),
192
+ project: {
193
+ name: "agent-app",
194
+ version: "1.2.3",
195
+ root,
196
+ packageManager: "bun",
197
+ configFile: null,
198
+ },
199
+ changedFiles: [],
200
+ gitAvailable: false,
201
+ notes: [],
202
+ ok: false,
203
+ checks: [],
204
+ diagnostics: [
205
+ {
206
+ code: "MANDU_VERIFY_MANIFEST_UNAVAILABLE",
207
+ severity: "warning",
208
+ title: "Routes manifest unavailable",
209
+ cause: "missing",
210
+ suggestedFix: {
211
+ type: "run_command",
212
+ command: "mandu build",
213
+ description: "Build the project.",
214
+ },
215
+ repairable: true,
216
+ source: "test",
217
+ },
218
+ ],
219
+ suggestedCommands: [],
220
+ nextRepairInput: ".mandu/agent-verify.json",
221
+ });
222
+
223
+ const report = await buildAgentRepairReport(root);
224
+ expect(report.status).toBe("ready");
225
+ expect(report.actions).toHaveLength(1);
226
+ expect(report.actions[0]).toMatchObject({
227
+ diagnosticCode: "MANDU_VERIFY_MANIFEST_UNAVAILABLE",
228
+ kind: "run_command",
229
+ command: "mandu build",
230
+ safeToApply: false,
231
+ });
232
+
233
+ const result = await writeAgentRepairReport(root, report);
234
+ expect(result.path).toBe(agentRepairReportPath(root));
235
+ expect(JSON.parse(await fs.readFile(result.path, "utf8")).status).toBe("ready");
236
+ });
237
+ });