@mandujs/core 0.54.2 → 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.
@@ -0,0 +1,308 @@
1
+ import type { DiagnoseReport } from "../diagnose";
2
+
3
+ export type AgentWorkflowStep = "context" | "plan" | "apply" | "verify" | "repair";
4
+
5
+ export type AgentDiagnosticSeverity = "info" | "warning" | "error" | "fatal";
6
+
7
+ export interface AgentSuggestedFix {
8
+ type: "run_command" | "create_file" | "modify_file" | "manual";
9
+ command?: string;
10
+ path?: string;
11
+ description: string;
12
+ }
13
+
14
+ export interface AgentDiagnostic {
15
+ code: string;
16
+ severity: AgentDiagnosticSeverity;
17
+ title: string;
18
+ file?: string;
19
+ line?: number;
20
+ cause: string;
21
+ suggestedFix?: AgentSuggestedFix;
22
+ docs?: string;
23
+ repairable: boolean;
24
+ source: string;
25
+ }
26
+
27
+ export interface AgentProjectSummary {
28
+ name: string | null;
29
+ version: string | null;
30
+ root: string;
31
+ packageManager: string;
32
+ configFile: string | null;
33
+ }
34
+
35
+ export interface AgentRouteSummary {
36
+ id: string;
37
+ pattern: string;
38
+ kind: "page" | "api" | "metadata" | string;
39
+ module: string;
40
+ methods?: string[];
41
+ hydration?: {
42
+ strategy?: string;
43
+ priority?: string;
44
+ };
45
+ hasClientModule: boolean;
46
+ hasContractModule: boolean;
47
+ layoutDepth: number;
48
+ metadataKind?: string;
49
+ }
50
+
51
+ export interface AgentArtifactSummary {
52
+ path: string;
53
+ kind: "partial" | "island" | "slot" | "contract";
54
+ }
55
+
56
+ export interface AgentGuardSummary {
57
+ preset: string | null;
58
+ customRules: number;
59
+ ruleOverrides: number;
60
+ }
61
+
62
+ export interface AgentEnvFileSummary {
63
+ path: string;
64
+ kind: "template" | "local" | "production" | "test" | "unknown";
65
+ redacted: true;
66
+ }
67
+
68
+ export interface AgentGitSummary {
69
+ branch: string | null;
70
+ changedFiles: string[];
71
+ statusAvailable: boolean;
72
+ }
73
+
74
+ export interface AgentCommandMap {
75
+ context: string;
76
+ manifest: string;
77
+ plan: string;
78
+ apply: string;
79
+ verify: string;
80
+ repair: string;
81
+ sync: string;
82
+ }
83
+
84
+ export interface AgentWorkflowCommand {
85
+ step: AgentWorkflowStep;
86
+ command: string;
87
+ purpose: string;
88
+ }
89
+
90
+ export interface AgentContext {
91
+ schemaVersion: 1;
92
+ framework: "mandu";
93
+ generatedAt: string;
94
+ project: AgentProjectSummary;
95
+ routeSource: "manifest" | "scanner" | "none";
96
+ routes: AgentRouteSummary[];
97
+ pages: AgentRouteSummary[];
98
+ apis: AgentRouteSummary[];
99
+ metadataRoutes: AgentRouteSummary[];
100
+ partials: AgentArtifactSummary[];
101
+ islands: AgentArtifactSummary[];
102
+ slots: AgentArtifactSummary[];
103
+ contracts: AgentArtifactSummary[];
104
+ guards: AgentGuardSummary;
105
+ env: AgentEnvFileSummary[];
106
+ deploy: {
107
+ intentFile: string | null;
108
+ targets: string[];
109
+ };
110
+ commands: AgentCommandMap;
111
+ agentWorkflow: {
112
+ canonical: AgentWorkflowStep[];
113
+ recommended: AgentWorkflowCommand[];
114
+ };
115
+ diagnose?: Pick<DiagnoseReport, "healthy" | "errorCount" | "warningCount" | "summary">;
116
+ diagnostics: AgentDiagnostic[];
117
+ git: AgentGitSummary;
118
+ warnings: string[];
119
+ }
120
+
121
+ export interface AgentManifest {
122
+ schemaVersion: 1;
123
+ framework: "mandu";
124
+ generatedAt: string;
125
+ project: AgentProjectSummary;
126
+ routeSource: AgentContext["routeSource"];
127
+ routes: AgentRouteSummary[];
128
+ apis: AgentRouteSummary[];
129
+ layouts: string[];
130
+ partials: AgentArtifactSummary[];
131
+ islands: AgentArtifactSummary[];
132
+ slots: AgentArtifactSummary[];
133
+ contracts: AgentArtifactSummary[];
134
+ guards: AgentGuardSummary;
135
+ env: AgentEnvFileSummary[];
136
+ deploy: AgentContext["deploy"];
137
+ commands: AgentCommandMap;
138
+ agentWorkflow: AgentContext["agentWorkflow"];
139
+ warnings: string[];
140
+ }
141
+
142
+ export interface BuildAgentContextOptions {
143
+ includeDiagnose?: boolean;
144
+ includeGit?: boolean;
145
+ }
146
+
147
+ export interface AgentVerifyCheck {
148
+ id: string;
149
+ label: string;
150
+ ok: boolean;
151
+ severity: AgentDiagnosticSeverity;
152
+ diagnostics: number;
153
+ details?: Record<string, unknown>;
154
+ }
155
+
156
+ export interface AgentSuggestedCommand {
157
+ command: string;
158
+ reason: string;
159
+ required: boolean;
160
+ }
161
+
162
+ export interface AgentVerifyReport {
163
+ schemaVersion: 1;
164
+ framework: "mandu";
165
+ generatedAt: string;
166
+ project: AgentProjectSummary;
167
+ changedFiles: string[];
168
+ gitAvailable: boolean;
169
+ notes: string[];
170
+ ok: boolean;
171
+ checks: AgentVerifyCheck[];
172
+ diagnostics: AgentDiagnostic[];
173
+ suggestedCommands: AgentSuggestedCommand[];
174
+ nextRepairInput: string;
175
+ }
176
+
177
+ export interface BuildAgentVerifyOptions {
178
+ changedOnly?: boolean;
179
+ includeDiagnose?: boolean;
180
+ includeGuard?: boolean;
181
+ includeContract?: boolean;
182
+ includeGit?: boolean;
183
+ base?: string;
184
+ staged?: boolean;
185
+ }
186
+
187
+ export type AgentRepairStatus = "ready" | "nothing_to_repair" | "input_missing";
188
+
189
+ export interface AgentRepairAction {
190
+ diagnosticCode: string;
191
+ kind: "run_command" | "manual" | "patch";
192
+ description: string;
193
+ command?: string;
194
+ file?: string;
195
+ safeToApply: boolean;
196
+ applied: boolean;
197
+ }
198
+
199
+ export interface AgentRepairReport {
200
+ schemaVersion: 1;
201
+ framework: "mandu";
202
+ generatedAt: string;
203
+ ok: boolean;
204
+ status: AgentRepairStatus;
205
+ sourceReport: string;
206
+ diagnostics: AgentDiagnostic[];
207
+ actions: AgentRepairAction[];
208
+ appliedActions: AgentRepairAction[];
209
+ warnings: string[];
210
+ nextVerifyCommand: string;
211
+ }
212
+
213
+ export interface BuildAgentRepairOptions {
214
+ from?: string;
215
+ apply?: boolean;
216
+ }
217
+
218
+ export type AgentDomain =
219
+ | "route"
220
+ | "api"
221
+ | "contract"
222
+ | "slot"
223
+ | "hydration"
224
+ | "guard"
225
+ | "testing"
226
+ | "deploy"
227
+ | "design"
228
+ | "docs"
229
+ | "db"
230
+ | "unknown";
231
+
232
+ export interface AgentPlanRisk {
233
+ level: "low" | "medium" | "high";
234
+ reason: string;
235
+ }
236
+
237
+ export interface AgentPlan {
238
+ schemaVersion: 1;
239
+ framework: "mandu";
240
+ generatedAt: string;
241
+ intent: string;
242
+ domains: AgentDomain[];
243
+ filesToRead: string[];
244
+ filesToCreate: string[];
245
+ filesToModify: string[];
246
+ mcpTools: string[];
247
+ risks: AgentPlanRisk[];
248
+ verification: AgentSuggestedCommand[];
249
+ notes: string[];
250
+ executable: false;
251
+ }
252
+
253
+ export interface BuildAgentPlanOptions {
254
+ intent: string;
255
+ }
256
+
257
+ export interface AgentApplyReport {
258
+ schemaVersion: 1;
259
+ framework: "mandu";
260
+ generatedAt: string;
261
+ ok: boolean;
262
+ dryRun: boolean;
263
+ sourcePlan: string;
264
+ intent: string;
265
+ domains: AgentDomain[];
266
+ actions: Array<{
267
+ kind: "mcp_tool" | "read_file" | "manual_edit" | "verify";
268
+ description: string;
269
+ tool?: string;
270
+ file?: string;
271
+ command?: string;
272
+ applied: false;
273
+ }>;
274
+ warnings: string[];
275
+ nextVerifyCommand: string;
276
+ }
277
+
278
+ export interface BuildAgentApplyOptions {
279
+ from?: string;
280
+ dryRun?: boolean;
281
+ }
282
+
283
+ export type AgentSyncTarget = "codex" | "claude" | "gemini" | "all";
284
+
285
+ export interface AgentSyncFile {
286
+ target: Exclude<AgentSyncTarget, "all">;
287
+ path: string;
288
+ action: "created" | "updated" | "unchanged" | "planned";
289
+ bytes: number;
290
+ }
291
+
292
+ export interface AgentSyncReport {
293
+ schemaVersion: 1;
294
+ framework: "mandu";
295
+ generatedAt: string;
296
+ ok: boolean;
297
+ target: AgentSyncTarget;
298
+ profile: "agent-core";
299
+ workflow: AgentWorkflowStep[];
300
+ files: AgentSyncFile[];
301
+ warnings: string[];
302
+ nextCommands: AgentSuggestedCommand[];
303
+ }
304
+
305
+ export interface BuildAgentSyncOptions {
306
+ target?: AgentSyncTarget;
307
+ dryRun?: boolean;
308
+ }
@@ -0,0 +1,406 @@
1
+ import fs from "fs/promises";
2
+ import path from "path";
3
+ import { runExtendedDiagnose } from "../diagnose";
4
+ import { runContractGuardCheck, runGuardCheck, type GuardViolation } from "../guard";
5
+ import { loadManifest } from "../spec/load";
6
+ import type { ContractViolation } from "../guard/contract-guard";
7
+ import {
8
+ buildAgentContext,
9
+ } from "./context";
10
+ import type {
11
+ AgentDiagnostic,
12
+ AgentDiagnosticSeverity,
13
+ AgentSuggestedCommand,
14
+ AgentVerifyCheck,
15
+ AgentVerifyReport,
16
+ BuildAgentVerifyOptions,
17
+ } from "./types";
18
+
19
+ const AGENT_VERIFY_RELATIVE_PATH = ".mandu/agent-verify.json";
20
+
21
+ function toPosix(value: string): string {
22
+ return value.replace(/\\/g, "/");
23
+ }
24
+
25
+ function normalizePath(value: string | undefined): string | undefined {
26
+ if (!value) return undefined;
27
+ return toPosix(value).replace(/^\.\//, "");
28
+ }
29
+
30
+ async function runGit(
31
+ rootDir: string,
32
+ args: string[],
33
+ timeoutMs = 5000,
34
+ ): Promise<{ ok: boolean; stdout: string; stderr: string }> {
35
+ if (typeof Bun === "undefined") {
36
+ return { ok: false, stdout: "", stderr: "Bun runtime unavailable" };
37
+ }
38
+ try {
39
+ const proc = Bun.spawn(["git", ...args], {
40
+ cwd: rootDir,
41
+ stdout: "pipe",
42
+ stderr: "pipe",
43
+ });
44
+ const timeout = new Promise<null>((resolve) => {
45
+ setTimeout(() => {
46
+ try {
47
+ proc.kill();
48
+ } catch {
49
+ // ignore
50
+ }
51
+ resolve(null);
52
+ }, timeoutMs);
53
+ });
54
+ const result = await Promise.race([
55
+ Promise.all([
56
+ new Response(proc.stdout).text(),
57
+ new Response(proc.stderr).text(),
58
+ proc.exited,
59
+ ]),
60
+ timeout,
61
+ ]);
62
+ if (result === null) return { ok: false, stdout: "", stderr: "git timed out" };
63
+ const [stdout, stderr, exitCode] = result;
64
+ return { ok: exitCode === 0, stdout, stderr };
65
+ } catch (err) {
66
+ return {
67
+ ok: false,
68
+ stdout: "",
69
+ stderr: err instanceof Error ? err.message : String(err),
70
+ };
71
+ }
72
+ }
73
+
74
+ function toLines(value: string): string[] {
75
+ return value
76
+ .split(/\r?\n/)
77
+ .map((line) => line.trim())
78
+ .filter(Boolean);
79
+ }
80
+
81
+ async function collectChangedFiles(rootDir: string, options: BuildAgentVerifyOptions): Promise<{
82
+ files: string[];
83
+ notes: string[];
84
+ gitAvailable: boolean;
85
+ }> {
86
+ if (options.includeGit === false) {
87
+ return {
88
+ files: [],
89
+ notes: ["Git collection disabled by caller."],
90
+ gitAvailable: false,
91
+ };
92
+ }
93
+
94
+ const notes: string[] = [];
95
+ const files = new Set<string>();
96
+ const inside = await runGit(rootDir, ["rev-parse", "--is-inside-work-tree"]);
97
+ if (!inside.ok || !inside.stdout.includes("true")) {
98
+ return {
99
+ files: [],
100
+ notes: ["Git repository not detected. Agent verify falls back to project-wide diagnostics."],
101
+ gitAvailable: false,
102
+ };
103
+ }
104
+
105
+ if (options.base) {
106
+ const diff = await runGit(rootDir, ["diff", "--name-only", `${options.base}...HEAD`]);
107
+ if (!diff.ok) {
108
+ notes.push(diff.stderr.trim() || `Failed to diff against base ${options.base}.`);
109
+ } else {
110
+ for (const file of toLines(diff.stdout)) files.add(normalizePath(file)!);
111
+ }
112
+ } else if (options.staged) {
113
+ const staged = await runGit(rootDir, ["diff", "--name-only", "--cached"]);
114
+ if (!staged.ok) {
115
+ notes.push(staged.stderr.trim() || "Failed to read staged diff.");
116
+ } else {
117
+ for (const file of toLines(staged.stdout)) files.add(normalizePath(file)!);
118
+ }
119
+ } else {
120
+ const [staged, unstaged, untracked] = await Promise.all([
121
+ runGit(rootDir, ["diff", "--name-only", "--cached"]),
122
+ runGit(rootDir, ["diff", "--name-only"]),
123
+ runGit(rootDir, ["ls-files", "--others", "--exclude-standard"]),
124
+ ]);
125
+ for (const result of [staged, unstaged, untracked]) {
126
+ if (!result.ok) {
127
+ notes.push(result.stderr.trim() || "Failed to collect one git change set.");
128
+ continue;
129
+ }
130
+ for (const file of toLines(result.stdout)) files.add(normalizePath(file)!);
131
+ }
132
+ }
133
+
134
+ return {
135
+ files: [...files].sort(),
136
+ notes,
137
+ gitAvailable: true,
138
+ };
139
+ }
140
+
141
+ function mapSeverity(value: string | undefined): AgentDiagnosticSeverity {
142
+ if (value === "warning" || value === "warn") return "warning";
143
+ if (value === "fatal") return "fatal";
144
+ if (value === "info") return "info";
145
+ return "error";
146
+ }
147
+
148
+ function code(prefix: string, raw: string): string {
149
+ return `${prefix}_${raw.toUpperCase().replace(/[^A-Z0-9]+/g, "_")}`;
150
+ }
151
+
152
+ function diagnoseDiagnostic(check: {
153
+ rule: string;
154
+ severity?: string;
155
+ message: string;
156
+ suggestion?: string;
157
+ }): AgentDiagnostic {
158
+ return {
159
+ code: code("MANDU_DIAGNOSE", check.rule),
160
+ severity: mapSeverity(check.severity ?? "error"),
161
+ title: check.rule,
162
+ cause: check.message,
163
+ ...(check.suggestion
164
+ ? {
165
+ suggestedFix: {
166
+ type: "manual" as const,
167
+ description: check.suggestion,
168
+ },
169
+ }
170
+ : {}),
171
+ docs: "docs/plans/20_agent_surface_consolidation_plan.md",
172
+ repairable: Boolean(check.suggestion),
173
+ source: "diagnose",
174
+ };
175
+ }
176
+
177
+ function guardDiagnostic(violation: GuardViolation, source: "guard" | "contract"): AgentDiagnostic {
178
+ return {
179
+ code: code(source === "guard" ? "MANDU_GUARD" : "MANDU_CONTRACT", violation.ruleId),
180
+ severity: mapSeverity(violation.severity),
181
+ title: violation.ruleId,
182
+ ...(normalizePath(violation.file) ? { file: normalizePath(violation.file) } : {}),
183
+ cause: violation.message,
184
+ ...(violation.suggestion
185
+ ? {
186
+ suggestedFix: {
187
+ type: "manual" as const,
188
+ description: violation.suggestion,
189
+ },
190
+ }
191
+ : {}),
192
+ docs: source === "guard" ? "docs/guides/07_agent_workflow.md" : "docs/resource-architecture.md",
193
+ repairable: Boolean(violation.suggestion),
194
+ source,
195
+ };
196
+ }
197
+
198
+ function matchesChanged(
199
+ diagnostic: AgentDiagnostic,
200
+ changed: Set<string>,
201
+ extraFiles: Array<string | undefined> = [],
202
+ ): boolean {
203
+ if (changed.size === 0) return true;
204
+ const candidates = [diagnostic.file, ...extraFiles]
205
+ .map(normalizePath)
206
+ .filter((value): value is string => Boolean(value));
207
+ if (candidates.length === 0) return true;
208
+ return candidates.some((file) => changed.has(file));
209
+ }
210
+
211
+ function commandSuggestions(changedFiles: string[], diagnostics: AgentDiagnostic[]): AgentSuggestedCommand[] {
212
+ const files = changedFiles.map(normalizePath).filter((value): value is string => Boolean(value));
213
+ const out: AgentSuggestedCommand[] = [];
214
+ const add = (command: string, reason: string, required: boolean) => {
215
+ if (!out.some((entry) => entry.command === command)) {
216
+ out.push({ command, reason, required });
217
+ }
218
+ };
219
+
220
+ if (files.length === 0) {
221
+ add("bun run typecheck", "No changed-file set was available; run the broad type gate.", true);
222
+ return out;
223
+ }
224
+
225
+ if (files.some((file) => /\.(ts|tsx|js|jsx)$/.test(file))) {
226
+ add("bun run typecheck", "TypeScript/JavaScript files changed.", true);
227
+ }
228
+
229
+ const changedTests = files.filter((file) => /\.test\.(ts|tsx|js|jsx)$/.test(file));
230
+ if (changedTests.length > 0) {
231
+ add(`bun test ${changedTests.join(" ")}`, "Changed test files should still pass directly.", true);
232
+ }
233
+
234
+ if (files.some((file) => file.startsWith("packages/core/"))) {
235
+ add("bun test packages/core/src packages/core/tests", "Core package files changed.", true);
236
+ }
237
+ if (files.some((file) => file.startsWith("packages/cli/"))) {
238
+ add("bun test packages/cli/src", "CLI package files changed.", true);
239
+ }
240
+ if (files.some((file) => file.startsWith("packages/mcp/"))) {
241
+ add("bun test packages/mcp/tests", "MCP package files changed.", true);
242
+ }
243
+ if (files.some((file) => file.includes("package.json") || file === "bun.lock")) {
244
+ add("bun run check:publish", "Package metadata or lockfile changed.", true);
245
+ }
246
+ if (diagnostics.some((d) => d.severity === "error" || d.severity === "fatal")) {
247
+ add("mandu agent repair --from .mandu/agent-verify.json", "Verification produced repairable diagnostics.", false);
248
+ }
249
+
250
+ return out;
251
+ }
252
+
253
+ function check(
254
+ id: string,
255
+ label: string,
256
+ diagnostics: AgentDiagnostic[],
257
+ details?: Record<string, unknown>,
258
+ ): AgentVerifyCheck {
259
+ const worst = diagnostics.some((d) => d.severity === "fatal")
260
+ ? "fatal"
261
+ : diagnostics.some((d) => d.severity === "error")
262
+ ? "error"
263
+ : diagnostics.some((d) => d.severity === "warning")
264
+ ? "warning"
265
+ : "info";
266
+ return {
267
+ id,
268
+ label,
269
+ ok: !diagnostics.some((d) => d.severity === "error" || d.severity === "fatal"),
270
+ severity: worst,
271
+ diagnostics: diagnostics.length,
272
+ ...(details ? { details } : {}),
273
+ };
274
+ }
275
+
276
+ export function agentVerifyReportPath(rootDir: string): string {
277
+ return path.join(rootDir, AGENT_VERIFY_RELATIVE_PATH);
278
+ }
279
+
280
+ export async function buildAgentVerifyReport(
281
+ rootDir: string = process.cwd(),
282
+ options: BuildAgentVerifyOptions = {},
283
+ ): Promise<AgentVerifyReport> {
284
+ const root = path.resolve(rootDir);
285
+ const changed = await collectChangedFiles(root, options);
286
+ const changedSet = new Set(changed.files.map(normalizePath).filter((value): value is string => Boolean(value)));
287
+ const context = await buildAgentContext(root, {
288
+ includeDiagnose: false,
289
+ includeGit: false,
290
+ });
291
+ const notes = [...changed.notes];
292
+ const checks: AgentVerifyCheck[] = [];
293
+ const diagnostics: AgentDiagnostic[] = [];
294
+
295
+ if (options.includeDiagnose !== false) {
296
+ try {
297
+ const report = await runExtendedDiagnose(root);
298
+ const diagnoseDiagnostics = report.checks
299
+ .filter((item) => !item.ok)
300
+ .map(diagnoseDiagnostic);
301
+ diagnostics.push(...diagnoseDiagnostics);
302
+ checks.push(check("diagnose", "Extended diagnose checks", diagnoseDiagnostics, {
303
+ healthy: report.healthy,
304
+ errorCount: report.errorCount,
305
+ warningCount: report.warningCount,
306
+ }));
307
+ } catch (err) {
308
+ const diag: AgentDiagnostic = {
309
+ code: "MANDU_VERIFY_DIAGNOSE_FAILED",
310
+ severity: "warning",
311
+ title: "Diagnose failed",
312
+ cause: err instanceof Error ? err.message : String(err),
313
+ suggestedFix: {
314
+ type: "run_command",
315
+ command: "mandu diagnose --json",
316
+ description: "Run diagnose directly to inspect the failure.",
317
+ },
318
+ docs: "docs/plans/20_agent_surface_consolidation_plan.md",
319
+ repairable: false,
320
+ source: "agent.verify",
321
+ };
322
+ diagnostics.push(diag);
323
+ checks.push(check("diagnose", "Extended diagnose checks", [diag]));
324
+ }
325
+ }
326
+
327
+ const manifestResult = await loadManifest(path.join(root, ".mandu", "routes.manifest.json"));
328
+ if (!manifestResult.success || !manifestResult.data) {
329
+ const diag: AgentDiagnostic = {
330
+ code: "MANDU_VERIFY_MANIFEST_UNAVAILABLE",
331
+ severity: "warning",
332
+ title: "Routes manifest unavailable",
333
+ cause: (manifestResult.errors ?? ["Routes manifest could not be loaded."]).join("; "),
334
+ suggestedFix: {
335
+ type: "run_command",
336
+ command: "mandu build",
337
+ description: "Build the project to refresh .mandu/routes.manifest.json.",
338
+ },
339
+ docs: "docs/guides/07_agent_workflow.md",
340
+ repairable: true,
341
+ source: "manifest",
342
+ };
343
+ diagnostics.push(diag);
344
+ checks.push(check("manifest", "Routes manifest load", [diag]));
345
+ } else {
346
+ checks.push(check("manifest", "Routes manifest load", [], {
347
+ routes: manifestResult.data.routes.length,
348
+ }));
349
+
350
+ if (options.includeGuard !== false) {
351
+ const guardResult = await runGuardCheck(manifestResult.data, root);
352
+ const guardDiagnostics = guardResult.violations
353
+ .map((violation) => guardDiagnostic(violation, "guard"))
354
+ .filter((diag) => !options.changedOnly || matchesChanged(diag, changedSet));
355
+ diagnostics.push(...guardDiagnostics);
356
+ checks.push(check("guard", "Architecture guard", guardDiagnostics, {
357
+ changedOnly: options.changedOnly !== false,
358
+ }));
359
+ }
360
+
361
+ if (options.includeContract !== false) {
362
+ const contractViolations: ContractViolation[] = await runContractGuardCheck(manifestResult.data, root);
363
+ const contractDiagnostics = contractViolations
364
+ .map((violation) => ({
365
+ violation,
366
+ diagnostic: guardDiagnostic(violation, "contract"),
367
+ }))
368
+ .filter(({ violation, diagnostic }) =>
369
+ !options.changedOnly ||
370
+ matchesChanged(diagnostic, changedSet, [violation.contractPath, violation.slotPath]),
371
+ )
372
+ .map(({ diagnostic }) => diagnostic);
373
+ diagnostics.push(...contractDiagnostics);
374
+ checks.push(check("contract", "Contract/slot consistency", contractDiagnostics, {
375
+ changedOnly: options.changedOnly !== false,
376
+ }));
377
+ }
378
+ }
379
+
380
+ const ok = !diagnostics.some((d) => d.severity === "error" || d.severity === "fatal");
381
+ return {
382
+ schemaVersion: 1,
383
+ framework: "mandu",
384
+ generatedAt: new Date().toISOString(),
385
+ project: context.project,
386
+ changedFiles: changed.files,
387
+ gitAvailable: changed.gitAvailable,
388
+ notes,
389
+ ok,
390
+ checks,
391
+ diagnostics,
392
+ suggestedCommands: commandSuggestions(changed.files, diagnostics),
393
+ nextRepairInput: AGENT_VERIFY_RELATIVE_PATH,
394
+ };
395
+ }
396
+
397
+ export async function writeAgentVerifyReport(
398
+ rootDir: string = process.cwd(),
399
+ report?: AgentVerifyReport,
400
+ ): Promise<{ path: string; report: AgentVerifyReport }> {
401
+ const value = report ?? await buildAgentVerifyReport(rootDir);
402
+ const outPath = agentVerifyReportPath(rootDir);
403
+ await fs.mkdir(path.dirname(outPath), { recursive: true });
404
+ await fs.writeFile(outPath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
405
+ return { path: outPath, report: value };
406
+ }