midnight-mcp 0.1.16 → 0.1.18

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,1208 @@
1
+ /**
2
+ * Contract validation handlers
3
+ * Pre-compilation validation for Compact contracts using the Compact CLI
4
+ */
5
+ import { exec, execFile } from "child_process";
6
+ import { promisify } from "util";
7
+ import { writeFile, mkdir, readFile, rm } from "fs/promises";
8
+ import { join, basename, resolve, isAbsolute } from "path";
9
+ import { tmpdir } from "os";
10
+ import { platform } from "process";
11
+ import { logger } from "../../utils/index.js";
12
+ const execAsync = promisify(exec);
13
+ const execFileAsync = promisify(execFile);
14
+ // ============================================================================
15
+ // SECURITY & VALIDATION HELPERS
16
+ // ============================================================================
17
+ /**
18
+ * Validate file path for security - prevent path traversal attacks
19
+ */
20
+ function validateFilePath(filePath) {
21
+ // Must be absolute path
22
+ if (!isAbsolute(filePath)) {
23
+ return {
24
+ valid: false,
25
+ error: "File path must be absolute (e.g., /Users/you/contract.compact)",
26
+ };
27
+ }
28
+ // Resolve to catch ../ traversal
29
+ const normalized = resolve(filePath);
30
+ // Check for path traversal attempts
31
+ // Simply check for ".." in the path - this is always suspicious in absolute paths
32
+ if (filePath.includes("..")) {
33
+ return {
34
+ valid: false,
35
+ error: "Path traversal detected - use absolute paths without ../",
36
+ };
37
+ }
38
+ // Must end with .compact
39
+ if (!normalized.endsWith(".compact")) {
40
+ return {
41
+ valid: false,
42
+ error: "File must have .compact extension",
43
+ };
44
+ }
45
+ // Block sensitive paths (Unix and Windows)
46
+ const blockedPathsUnix = ["/etc", "/var", "/usr", "/bin", "/sbin", "/root"];
47
+ const blockedPathsWindows = [
48
+ "C:\\Windows",
49
+ "C:\\Program Files",
50
+ "C:\\Program Files (x86)",
51
+ "C:\\System32",
52
+ "C:\\ProgramData",
53
+ ];
54
+ const blockedPaths = platform === "win32" ? blockedPathsWindows : blockedPathsUnix;
55
+ const normalizedLower = normalized.toLowerCase();
56
+ if (blockedPaths.some((blocked) => normalizedLower.startsWith(blocked.toLowerCase()))) {
57
+ return {
58
+ valid: false,
59
+ error: "Cannot access system directories",
60
+ };
61
+ }
62
+ return { valid: true, normalizedPath: normalized };
63
+ }
64
+ /**
65
+ * Check if content is valid UTF-8 text (not binary)
66
+ */
67
+ function isValidUtf8Text(content) {
68
+ // Check for null bytes (common in binary files)
69
+ if (content.includes("\x00")) {
70
+ return false;
71
+ }
72
+ // Check for excessive non-printable characters
73
+ const nonPrintable = content.match(/[\x00-\x08\x0B\x0C\x0E-\x1F]/g);
74
+ if (nonPrintable && nonPrintable.length > content.length * 0.01) {
75
+ return false;
76
+ }
77
+ return true;
78
+ }
79
+ /**
80
+ * Detect local includes that won't work in temp directory
81
+ */
82
+ function detectLocalIncludes(code) {
83
+ const localIncludes = [];
84
+ // Pattern: include "something.compact" or include "./path"
85
+ const includePattern = /include\s+"([^"]+)"/g;
86
+ let match;
87
+ while ((match = includePattern.exec(code)) !== null) {
88
+ const includePath = match[1];
89
+ // Skip standard library includes
90
+ if (includePath === "std" ||
91
+ includePath.startsWith("CompactStandardLibrary")) {
92
+ continue;
93
+ }
94
+ // Local file reference
95
+ if (includePath.endsWith(".compact") ||
96
+ includePath.startsWith("./") ||
97
+ includePath.startsWith("../")) {
98
+ localIncludes.push(includePath);
99
+ }
100
+ }
101
+ return localIncludes;
102
+ }
103
+ // ============================================================================
104
+ // VALIDATION HANDLERS
105
+ // ============================================================================
106
+ /**
107
+ * Validate a Compact contract by running the compiler
108
+ * This provides pre-compilation validation with detailed error diagnostics
109
+ */
110
+ export async function validateContract(input) {
111
+ logger.debug("Validating contract", {
112
+ filename: input.filename,
113
+ hasCode: !!input.code,
114
+ filePath: input.filePath,
115
+ });
116
+ // ============================================================================
117
+ // RESOLVE CODE SOURCE - Either from code string or file path
118
+ // ============================================================================
119
+ let code;
120
+ let filename;
121
+ let sourceDir = null; // Track source directory for local includes
122
+ let originalFilePath = null; // Track original file path for compilation
123
+ if (input.filePath) {
124
+ // SECURITY: Validate file path first
125
+ const pathValidation = validateFilePath(input.filePath);
126
+ if (!pathValidation.valid) {
127
+ return {
128
+ success: false,
129
+ errorType: "security_error",
130
+ error: "Invalid file path",
131
+ message: `❌ ${pathValidation.error}`,
132
+ userAction: {
133
+ problem: pathValidation.error,
134
+ solution: "Provide an absolute path to a .compact file in your project directory",
135
+ example: { filePath: "/Users/you/projects/myapp/contract.compact" },
136
+ isUserFault: true,
137
+ },
138
+ };
139
+ }
140
+ const safePath = pathValidation.normalizedPath;
141
+ sourceDir = join(safePath, "..");
142
+ originalFilePath = safePath; // Store for use in compilation
143
+ // SECURITY: Validate sourceDir against blocked paths
144
+ // This prevents malicious includes from accessing system directories
145
+ const sourceDirValidation = validateFilePath(join(sourceDir, "dummy.compact"));
146
+ if (!sourceDirValidation.valid &&
147
+ sourceDirValidation.error?.includes("system directories")) {
148
+ return {
149
+ success: false,
150
+ errorType: "security_error",
151
+ error: "Invalid source directory",
152
+ message: "❌ Cannot access files in system directories",
153
+ userAction: {
154
+ problem: "The contract's parent directory is a restricted system location",
155
+ solution: "Move your contract files to a user project directory",
156
+ isUserFault: true,
157
+ },
158
+ };
159
+ }
160
+ // Read code from file
161
+ try {
162
+ code = await readFile(safePath, "utf-8");
163
+ filename = basename(safePath);
164
+ // SECURITY: Check for binary/non-UTF8 content
165
+ if (!isValidUtf8Text(code)) {
166
+ return {
167
+ success: false,
168
+ errorType: "user_error",
169
+ error: "Invalid file content",
170
+ message: "❌ File appears to be binary or contains invalid characters",
171
+ userAction: {
172
+ problem: "The file is not a valid UTF-8 text file",
173
+ solution: "Ensure you're pointing to a Compact source file (.compact), not a compiled binary",
174
+ isUserFault: true,
175
+ },
176
+ };
177
+ }
178
+ }
179
+ catch (fsError) {
180
+ const err = fsError;
181
+ return {
182
+ success: false,
183
+ errorType: "user_error",
184
+ error: "Failed to read file",
185
+ message: `❌ Cannot read file: ${input.filePath}`,
186
+ userAction: {
187
+ problem: err.code === "ENOENT"
188
+ ? "File does not exist"
189
+ : err.code === "EACCES"
190
+ ? "Permission denied"
191
+ : "Cannot read file",
192
+ solution: err.code === "ENOENT"
193
+ ? "Check that the file path is correct"
194
+ : "Check file permissions",
195
+ details: err.message,
196
+ isUserFault: true,
197
+ },
198
+ };
199
+ }
200
+ }
201
+ else if (input.code) {
202
+ code = input.code;
203
+ // Sanitize filename to prevent command injection
204
+ const rawFilename = input.filename || "contract.compact";
205
+ filename = rawFilename.replace(/[^a-zA-Z0-9._-]/g, "_");
206
+ if (!filename.endsWith(".compact")) {
207
+ filename = "contract.compact";
208
+ }
209
+ // Check for binary content in provided code
210
+ if (!isValidUtf8Text(code)) {
211
+ return {
212
+ success: false,
213
+ errorType: "user_error",
214
+ error: "Invalid code content",
215
+ message: "❌ Code contains invalid characters",
216
+ userAction: {
217
+ problem: "The provided code contains binary or non-printable characters",
218
+ solution: "Provide valid UTF-8 Compact source code",
219
+ isUserFault: true,
220
+ },
221
+ };
222
+ }
223
+ }
224
+ else {
225
+ // Neither code nor filePath provided
226
+ return {
227
+ success: false,
228
+ errorType: "user_error",
229
+ error: "No contract provided",
230
+ message: "❌ Must provide either 'code' or 'filePath'",
231
+ userAction: {
232
+ problem: "Neither code string nor file path was provided",
233
+ solution: "Provide the contract source code OR a path to a .compact file",
234
+ example: {
235
+ withCode: { code: "pragma language_version >= 0.16; ..." },
236
+ withFile: { filePath: "/path/to/contract.compact" },
237
+ },
238
+ isUserFault: true,
239
+ },
240
+ };
241
+ }
242
+ // ============================================================================
243
+ // INPUT VALIDATION - Check for user errors before attempting compilation
244
+ // ============================================================================
245
+ // Check for local includes that won't work in temp directory
246
+ const localIncludes = detectLocalIncludes(code);
247
+ if (localIncludes.length > 0 && !sourceDir) {
248
+ // Code was provided directly (not from file) and has local includes
249
+ return {
250
+ success: false,
251
+ errorType: "user_error",
252
+ error: "Local includes detected",
253
+ message: "❌ Contract has local file includes that cannot be resolved",
254
+ userAction: {
255
+ problem: `Contract includes local files: ${localIncludes.join(", ")}`,
256
+ solution: "Use filePath instead of code when your contract has local includes, so we can resolve relative paths",
257
+ detectedIncludes: localIncludes,
258
+ example: {
259
+ instead: '{ code: "include \\"utils.compact\\"; ..." }',
260
+ use: '{ filePath: "/path/to/your/contract.compact" }',
261
+ },
262
+ isUserFault: true,
263
+ },
264
+ };
265
+ }
266
+ // Warn about local includes (they may fail during compilation)
267
+ const localIncludeWarning = localIncludes.length > 0
268
+ ? {
269
+ warning: "Contract has local includes",
270
+ includes: localIncludes,
271
+ note: "Local includes may fail if files are not in the expected location relative to the contract",
272
+ }
273
+ : null;
274
+ // Check for empty input
275
+ if (!code || code.trim().length === 0) {
276
+ return {
277
+ success: false,
278
+ errorType: "user_error",
279
+ error: "Empty contract code provided",
280
+ message: "❌ No contract code to validate",
281
+ userAction: {
282
+ problem: "The contract code is empty or contains only whitespace",
283
+ solution: "Provide valid Compact contract source code",
284
+ example: `pragma language_version >= 0.16;
285
+
286
+ import CompactStandardLibrary;
287
+
288
+ export ledger counter: Counter;
289
+
290
+ export circuit increment(): [] {
291
+ counter.increment(1);
292
+ }`,
293
+ },
294
+ };
295
+ }
296
+ // Check for excessively large input (potential abuse or mistake)
297
+ const MAX_CODE_SIZE = 1024 * 1024; // 1MB
298
+ if (code.length > MAX_CODE_SIZE) {
299
+ return {
300
+ success: false,
301
+ errorType: "user_error",
302
+ error: "Contract code too large",
303
+ message: "❌ Contract code exceeds maximum size",
304
+ userAction: {
305
+ problem: `Contract is ${(code.length / 1024).toFixed(1)}KB, maximum is ${MAX_CODE_SIZE / 1024}KB`,
306
+ solution: "Reduce contract size or split into multiple files",
307
+ },
308
+ };
309
+ }
310
+ // Check for missing pragma (common user mistake)
311
+ if (!code.includes("pragma language_version")) {
312
+ return {
313
+ success: false,
314
+ errorType: "user_error",
315
+ error: "Missing pragma directive",
316
+ message: "❌ Contract is missing required pragma directive",
317
+ userAction: {
318
+ problem: "All Compact contracts must start with a pragma language_version directive",
319
+ solution: "Add pragma directive at the beginning of your contract",
320
+ fix: "Add: pragma language_version >= 0.16;",
321
+ example: `pragma language_version >= 0.16;
322
+
323
+ import CompactStandardLibrary;
324
+
325
+ // ... rest of your contract`,
326
+ },
327
+ detectedIssues: ["Missing pragma language_version directive"],
328
+ };
329
+ }
330
+ // Check for missing import (common for Counter, Map, etc.)
331
+ // Use word boundaries to avoid false positives in comments/strings
332
+ const usesStdLib = /\bCounter\b/.test(code) ||
333
+ /\bMap\s*</.test(code) ||
334
+ /\bSet\s*</.test(code) ||
335
+ /\bOpaque\s*</.test(code);
336
+ const hasImport = /\bimport\s+CompactStandardLibrary\b/.test(code) ||
337
+ /\binclude\s+"std"/.test(code);
338
+ if (usesStdLib && !hasImport) {
339
+ return {
340
+ success: false,
341
+ errorType: "user_error",
342
+ error: "Missing standard library import",
343
+ message: "❌ Contract uses standard library types without importing them",
344
+ userAction: {
345
+ problem: "You're using types like Counter, Map, Set, or Opaque without importing the standard library",
346
+ solution: "Add the import statement after your pragma directive",
347
+ fix: "Add: import CompactStandardLibrary;",
348
+ example: `pragma language_version >= 0.16;
349
+
350
+ import CompactStandardLibrary;
351
+
352
+ export ledger counter: Counter;
353
+ // ...`,
354
+ },
355
+ detectedIssues: [
356
+ "Uses standard library types (Counter, Map, Set, Opaque)",
357
+ "Missing: import CompactStandardLibrary;",
358
+ ],
359
+ };
360
+ }
361
+ // ============================================================================
362
+ // COMPILER CHECK - Verify compiler is available
363
+ // ============================================================================
364
+ let compactPath = "";
365
+ let compilerVersion = "";
366
+ try {
367
+ if (platform === "win32") {
368
+ // On Windows, avoid the built-in NTFS 'compact.exe' from System32
369
+ // by iterating through all candidates and verifying each one
370
+ const { stdout: whereOutput } = await execAsync("where compact.exe");
371
+ const candidates = whereOutput
372
+ .trim()
373
+ .split(/\r?\n/)
374
+ .filter((line) => line.trim().length > 0);
375
+ let found = false;
376
+ for (const candidate of candidates) {
377
+ try {
378
+ const candidatePath = candidate.trim();
379
+ // Skip Windows System32 compact.exe (NTFS compression utility)
380
+ if (candidatePath.toLowerCase().includes("system32")) {
381
+ continue;
382
+ }
383
+ const { stdout: versionOutput } = await execFileAsync(candidatePath, [
384
+ "compile",
385
+ "--version",
386
+ ]);
387
+ compactPath = candidatePath;
388
+ compilerVersion = versionOutput.trim();
389
+ found = true;
390
+ break;
391
+ }
392
+ catch {
393
+ // Try next candidate
394
+ }
395
+ }
396
+ if (!found) {
397
+ throw new Error("Compact compiler not found in PATH");
398
+ }
399
+ }
400
+ else {
401
+ // Unix: use which to find compact
402
+ const { stdout: whichOutput } = await execAsync("which compact");
403
+ compactPath = whichOutput.trim().split(/\r?\n/)[0];
404
+ const { stdout: versionOutput } = await execFileAsync(compactPath, [
405
+ "compile",
406
+ "--version",
407
+ ]);
408
+ compilerVersion = versionOutput.trim();
409
+ }
410
+ }
411
+ catch {
412
+ return {
413
+ success: false,
414
+ errorType: "environment_error",
415
+ compilerInstalled: false,
416
+ error: "Compact compiler not found",
417
+ message: "❌ Compact compiler is not installed",
418
+ installation: {
419
+ message: "The Compact compiler is required for contract validation. Install it with:",
420
+ command: `curl --proto '=https' --tlsv1.2 -LsSf https://github.com/midnightntwrk/compact/releases/latest/download/compact-installer.sh | sh`,
421
+ postInstall: [
422
+ "After installation, run: compact update",
423
+ "Then verify with: compact compile --version",
424
+ ],
425
+ docs: "https://docs.midnight.network/develop/tutorial/building",
426
+ },
427
+ userAction: {
428
+ problem: "The Compact compiler is not installed on this system",
429
+ solution: "Install the compiler using the command above, then retry validation",
430
+ isUserFault: false,
431
+ },
432
+ };
433
+ }
434
+ // Check compiler version compatibility
435
+ const versionMatch = compilerVersion.match(/(\d+)\.(\d+)/);
436
+ if (versionMatch) {
437
+ const major = parseInt(versionMatch[1], 10);
438
+ const minor = parseInt(versionMatch[2], 10);
439
+ if (major === 0 && minor < 16) {
440
+ return {
441
+ success: false,
442
+ errorType: "environment_error",
443
+ compilerInstalled: true,
444
+ compilerVersion,
445
+ error: "Compiler version too old",
446
+ message: `❌ Compact compiler ${compilerVersion} is outdated`,
447
+ userAction: {
448
+ problem: `Your compiler version (${compilerVersion}) may not support current syntax`,
449
+ solution: "Update to the latest compiler version",
450
+ command: "compact update",
451
+ isUserFault: false,
452
+ },
453
+ };
454
+ }
455
+ }
456
+ // ============================================================================
457
+ // COMPILATION - Create temp files and run compiler
458
+ // ============================================================================
459
+ const tempDir = join(tmpdir(), `midnight-validate-${Date.now()}`);
460
+ const contractPath = join(tempDir, filename);
461
+ const outputDir = join(tempDir, "output");
462
+ try {
463
+ // Create temp directory
464
+ try {
465
+ await mkdir(tempDir, { recursive: true });
466
+ await mkdir(outputDir, { recursive: true });
467
+ }
468
+ catch (fsError) {
469
+ const err = fsError;
470
+ return {
471
+ success: false,
472
+ errorType: "system_error",
473
+ error: "Failed to create temporary directory",
474
+ message: "❌ System error: Cannot create temp files",
475
+ systemError: {
476
+ code: err.code,
477
+ details: err.message,
478
+ problem: err.code === "ENOSPC"
479
+ ? "Disk is full"
480
+ : err.code === "EACCES"
481
+ ? "Permission denied"
482
+ : "File system error",
483
+ solution: err.code === "ENOSPC"
484
+ ? "Free up disk space and retry"
485
+ : err.code === "EACCES"
486
+ ? "Check file system permissions"
487
+ : "Check system resources",
488
+ isUserFault: false,
489
+ },
490
+ };
491
+ }
492
+ // Write contract file
493
+ try {
494
+ await writeFile(contractPath, code, "utf-8");
495
+ }
496
+ catch (writeError) {
497
+ const err = writeError;
498
+ return {
499
+ success: false,
500
+ errorType: "system_error",
501
+ error: "Failed to write contract file",
502
+ message: "❌ System error: Cannot write temp file",
503
+ systemError: {
504
+ code: err.code,
505
+ details: err.message,
506
+ isUserFault: false,
507
+ },
508
+ };
509
+ }
510
+ // Run compilation
511
+ // When originalFilePath is available (file path provided), compile the original file
512
+ // from its source directory to resolve local includes correctly.
513
+ // Otherwise, compile the temp file from the temp directory.
514
+ const fileToCompile = originalFilePath || contractPath;
515
+ const compileCwd = originalFilePath ? sourceDir : tempDir;
516
+ try {
517
+ const execOptions = {
518
+ timeout: 60000, // 60 second timeout
519
+ maxBuffer: 10 * 1024 * 1024, // 10MB buffer
520
+ cwd: compileCwd, // Use appropriate directory for include resolution
521
+ };
522
+ // Use execFile with array arguments to avoid shell injection vulnerabilities
523
+ // This is safer than string interpolation as paths are passed directly
524
+ const { stdout, stderr } = await execFileAsync("compact", ["compile", fileToCompile, outputDir], execOptions);
525
+ // Compilation succeeded!
526
+ const allWarnings = stderr ? parseWarnings(stderr) : [];
527
+ // Add local include warning if applicable
528
+ if (localIncludeWarning) {
529
+ allWarnings.push(`Note: Contract has local includes (${localIncludes.join(", ")}) - ensure these files exist relative to your contract`);
530
+ }
531
+ return {
532
+ success: true,
533
+ errorType: null,
534
+ compilerInstalled: true,
535
+ compilerVersion,
536
+ compilerPath: compactPath,
537
+ message: "✅ Contract compiled successfully!",
538
+ output: stdout || "Compilation completed without errors",
539
+ warnings: allWarnings,
540
+ localIncludes: localIncludes.length > 0 ? localIncludes : undefined,
541
+ contractInfo: {
542
+ filename,
543
+ codeLength: code.length,
544
+ lineCount: code.split("\n").length,
545
+ },
546
+ nextSteps: [
547
+ "The contract syntax is valid and compiles",
548
+ "Generated files would be in the output directory",
549
+ "You can proceed with deployment or further development",
550
+ ],
551
+ };
552
+ }
553
+ catch (compileError) {
554
+ // Compilation failed - parse and categorize the error
555
+ const error = compileError;
556
+ // Check for timeout
557
+ if (error.killed || error.signal === "SIGTERM") {
558
+ return {
559
+ success: false,
560
+ errorType: "timeout_error",
561
+ compilerInstalled: true,
562
+ compilerVersion,
563
+ error: "Compilation timed out",
564
+ message: "❌ Compilation timed out after 60 seconds",
565
+ userAction: {
566
+ problem: "The contract took too long to compile",
567
+ solution: "Simplify the contract or check for infinite loops in circuit logic",
568
+ possibleCauses: [
569
+ "Very complex contract with many circuits",
570
+ "Recursive or deeply nested structures",
571
+ "Large number of constraints",
572
+ ],
573
+ isUserFault: true,
574
+ },
575
+ };
576
+ }
577
+ const errorOutput = error.stderr || error.stdout || error.message || "";
578
+ const diagnostics = parseCompilerErrors(errorOutput, code);
579
+ // Categorize the error for better user feedback
580
+ const errorCategory = categorizeCompilerError(errorOutput);
581
+ return {
582
+ success: false,
583
+ errorType: "compilation_error",
584
+ errorCategory,
585
+ compilerInstalled: true,
586
+ compilerVersion,
587
+ compilerPath: compactPath,
588
+ message: `❌ ${errorCategory.title}`,
589
+ errors: diagnostics.errors,
590
+ errorCount: diagnostics.errors.length,
591
+ rawOutput: errorOutput.slice(0, 2000),
592
+ contractInfo: {
593
+ filename,
594
+ codeLength: code.length,
595
+ lineCount: code.split("\n").length,
596
+ },
597
+ userAction: {
598
+ problem: errorCategory.explanation,
599
+ solution: errorCategory.solution,
600
+ isUserFault: true,
601
+ },
602
+ suggestions: diagnostics.suggestions,
603
+ commonFixes: getCommonFixes(diagnostics.errors),
604
+ };
605
+ }
606
+ }
607
+ finally {
608
+ // Cleanup temp files (cross-platform)
609
+ try {
610
+ await rm(tempDir, { recursive: true, force: true });
611
+ }
612
+ catch {
613
+ // Ignore cleanup errors
614
+ }
615
+ }
616
+ }
617
+ // ============================================================================
618
+ // ERROR PARSING HELPERS
619
+ // ============================================================================
620
+ /**
621
+ * Categorize compiler errors for better user feedback
622
+ */
623
+ function categorizeCompilerError(output) {
624
+ const lowerOutput = output.toLowerCase();
625
+ if (lowerOutput.includes("parse error") ||
626
+ lowerOutput.includes("looking for")) {
627
+ return {
628
+ category: "syntax_error",
629
+ title: "Syntax Error",
630
+ explanation: "The contract has invalid syntax that the parser cannot understand",
631
+ solution: "Check for missing semicolons, brackets, or typos near the indicated line",
632
+ };
633
+ }
634
+ if (lowerOutput.includes("type") &&
635
+ (lowerOutput.includes("mismatch") || lowerOutput.includes("expected"))) {
636
+ return {
637
+ category: "type_error",
638
+ title: "Type Error",
639
+ explanation: "There is a type mismatch in your contract",
640
+ solution: "Ensure variable types match expected types in operations",
641
+ };
642
+ }
643
+ if (lowerOutput.includes("undefined") ||
644
+ lowerOutput.includes("not found") ||
645
+ lowerOutput.includes("unknown")) {
646
+ return {
647
+ category: "reference_error",
648
+ title: "Reference Error",
649
+ explanation: "The contract references something that doesn't exist",
650
+ solution: "Check that all variables, types, and functions are properly defined or imported",
651
+ };
652
+ }
653
+ if (lowerOutput.includes("import") ||
654
+ lowerOutput.includes("include") ||
655
+ lowerOutput.includes("module")) {
656
+ return {
657
+ category: "import_error",
658
+ title: "Import Error",
659
+ explanation: "There is a problem with an import or include statement",
660
+ solution: "Verify import paths and ensure required libraries are available",
661
+ };
662
+ }
663
+ if (lowerOutput.includes("circuit") ||
664
+ lowerOutput.includes("witness") ||
665
+ lowerOutput.includes("ledger")) {
666
+ return {
667
+ category: "structure_error",
668
+ title: "Contract Structure Error",
669
+ explanation: "There is an issue with the contract structure (circuits, witnesses, or ledger)",
670
+ solution: "Review the contract structure against Compact documentation",
671
+ };
672
+ }
673
+ return {
674
+ category: "unknown_error",
675
+ title: "Compilation Failed",
676
+ explanation: "The compiler encountered an error",
677
+ solution: "Review the error message and check Compact documentation",
678
+ };
679
+ }
680
+ /**
681
+ * Parse compiler error output into structured diagnostics
682
+ */
683
+ function parseCompilerErrors(output, sourceCode) {
684
+ const errors = [];
685
+ const suggestions = [];
686
+ const lines = sourceCode.split("\n");
687
+ // Common patterns in Compact compiler output
688
+ // Pattern: "error: <message>" or "Error: <message>"
689
+ const errorLinePattern = /(?:error|Error):\s*(.+)/gi;
690
+ // Pattern: "line <n>:" or "at line <n>" or "<filename>:<line>:<col>"
691
+ const lineNumberPattern = /(?:line\s*(\d+)|at\s+line\s+(\d+)|:(\d+):(\d+))/i;
692
+ // Pattern: "expected <x>, found <y>"
693
+ const expectedPattern = /expected\s+['"`]?([^'"`]+)['"`]?,?\s*(?:found|got)\s+['"`]?([^'"`]+)['"`]?/i;
694
+ // Split output into logical segments
695
+ const segments = output.split(/(?=error:|Error:)/i);
696
+ for (const segment of segments) {
697
+ if (!segment.trim())
698
+ continue;
699
+ const errorMatch = segment.match(errorLinePattern);
700
+ if (errorMatch) {
701
+ const message = errorMatch[0].replace(/^(?:error|Error):\s*/i, "").trim();
702
+ // Try to extract line number
703
+ const lineMatch = segment.match(lineNumberPattern);
704
+ const line = lineMatch
705
+ ? parseInt(lineMatch[1] || lineMatch[2] || lineMatch[3], 10)
706
+ : undefined;
707
+ const column = lineMatch && lineMatch[4] ? parseInt(lineMatch[4], 10) : undefined;
708
+ // Get source context if we have a line number
709
+ let context;
710
+ if (line && line > 0 && line <= lines.length) {
711
+ const start = Math.max(0, line - 2);
712
+ const end = Math.min(lines.length, line + 1);
713
+ context = lines
714
+ .slice(start, end)
715
+ .map((l, i) => `${start + i + 1}: ${l}`)
716
+ .join("\n");
717
+ }
718
+ errors.push({
719
+ line,
720
+ column,
721
+ message,
722
+ severity: "error",
723
+ context,
724
+ });
725
+ // Generate suggestions based on error type
726
+ const expectedMatch = message.match(expectedPattern);
727
+ if (expectedMatch) {
728
+ suggestions.push(`Expected "${expectedMatch[1]}" but found "${expectedMatch[2]}". Check your syntax.`);
729
+ }
730
+ }
731
+ }
732
+ // If no structured errors found, add the raw output as an error
733
+ if (errors.length === 0 && output.trim()) {
734
+ errors.push({
735
+ message: output.trim().slice(0, 500),
736
+ severity: "error",
737
+ });
738
+ }
739
+ // Add general suggestions based on common issues
740
+ if (output.includes("Cell")) {
741
+ suggestions.push("Remember to use .value to access Cell<T> contents (e.g., state.value)");
742
+ }
743
+ if (output.includes("Opaque")) {
744
+ suggestions.push('Opaque<"string"> is a type, not a type alias. Use it directly in signatures.');
745
+ }
746
+ if (output.includes("disclose")) {
747
+ suggestions.push("In conditionals, use: const x = disclose(expr); if (x) { ... } instead of if (disclose(expr))");
748
+ }
749
+ if (output.includes("Counter")) {
750
+ suggestions.push("Counter type requires initialization: counter = Counter.increment(counter, 1)");
751
+ }
752
+ return { errors, suggestions };
753
+ }
754
+ /**
755
+ * Parse warnings from compiler output
756
+ */
757
+ function parseWarnings(output) {
758
+ const warnings = [];
759
+ const warningPattern = /(?:warning|Warning):\s*(.+)/gi;
760
+ let match;
761
+ while ((match = warningPattern.exec(output)) !== null) {
762
+ warnings.push(match[1].trim());
763
+ }
764
+ return warnings;
765
+ }
766
+ /**
767
+ * Get common fixes based on error patterns
768
+ */
769
+ function getCommonFixes(errors) {
770
+ const fixes = [];
771
+ const messages = errors.map((e) => e.message.toLowerCase()).join(" ");
772
+ if (messages.includes("cell") || messages.includes("value")) {
773
+ fixes.push({
774
+ pattern: "Cell<T> access error",
775
+ fix: "Use `state.value` instead of just `state` when accessing Cell contents",
776
+ });
777
+ }
778
+ if (messages.includes("opaque") || messages.includes("string type")) {
779
+ fixes.push({
780
+ pattern: "Opaque string type error",
781
+ fix: 'Use `Opaque<"your_type_name">` directly - it cannot be aliased with type keyword',
782
+ });
783
+ }
784
+ if (messages.includes("boolean") || messages.includes("witness")) {
785
+ fixes.push({
786
+ pattern: "Boolean witness error",
787
+ fix: "Witnesses return `Uint<1>` not `Boolean` - use `x != 0` to convert to Boolean",
788
+ });
789
+ }
790
+ if (messages.includes("disclose") || messages.includes("conditional")) {
791
+ fixes.push({
792
+ pattern: "Disclosure in conditional error",
793
+ fix: "Store disclose() result in const before using in if: `const revealed = disclose(x); if (revealed) { ... }`",
794
+ });
795
+ }
796
+ if (messages.includes("counter") || messages.includes("increment")) {
797
+ fixes.push({
798
+ pattern: "Counter initialization error",
799
+ fix: "Initialize counters with: `counter = Counter.increment(counter, 1)`",
800
+ });
801
+ }
802
+ if (messages.includes("map") ||
803
+ messages.includes("key") ||
804
+ messages.includes("insert")) {
805
+ fixes.push({
806
+ pattern: "Map operation error",
807
+ fix: "Maps require aligned access: insert at key before reading, or use default values",
808
+ });
809
+ }
810
+ return fixes;
811
+ }
812
+ // ============================================================================
813
+ // CONTRACT STRUCTURE EXTRACTION
814
+ // ============================================================================
815
+ /**
816
+ * Extract the structure of a Compact contract (circuits, witnesses, ledger, etc.)
817
+ * This helps agents understand what a contract does without parsing it themselves
818
+ */
819
+ export async function extractContractStructure(input) {
820
+ logger.debug("Extracting contract structure", {
821
+ hasCode: !!input.code,
822
+ filePath: input.filePath,
823
+ });
824
+ // Resolve code source
825
+ let code;
826
+ let filename;
827
+ if (input.filePath) {
828
+ // SECURITY: Validate file path
829
+ const pathValidation = validateFilePath(input.filePath);
830
+ if (!pathValidation.valid) {
831
+ return {
832
+ success: false,
833
+ error: "Invalid file path",
834
+ message: pathValidation.error,
835
+ };
836
+ }
837
+ try {
838
+ code = await readFile(pathValidation.normalizedPath, "utf-8");
839
+ filename = basename(pathValidation.normalizedPath);
840
+ // Check for binary content
841
+ if (!isValidUtf8Text(code)) {
842
+ return {
843
+ success: false,
844
+ error: "Invalid file content",
845
+ message: "File appears to be binary or contains invalid characters",
846
+ };
847
+ }
848
+ }
849
+ catch (fsError) {
850
+ const err = fsError;
851
+ return {
852
+ success: false,
853
+ error: "Failed to read file",
854
+ message: `Cannot read file: ${input.filePath}`,
855
+ details: err.code === "ENOENT" ? "File does not exist" : err.message,
856
+ };
857
+ }
858
+ }
859
+ else if (input.code) {
860
+ code = input.code;
861
+ filename = "contract.compact";
862
+ // Check for binary content
863
+ if (!isValidUtf8Text(code)) {
864
+ return {
865
+ success: false,
866
+ error: "Invalid code content",
867
+ message: "Code contains invalid characters",
868
+ };
869
+ }
870
+ }
871
+ else {
872
+ return {
873
+ success: false,
874
+ error: "No contract provided",
875
+ message: "Must provide either 'code' or 'filePath'",
876
+ };
877
+ }
878
+ // Extract pragma version (supports >=, >, <=, <, ==, ~; >=? and <=? are ordered
879
+ // so that >= and <= are matched before > and <)
880
+ const pragmaMatch = code.match(/pragma\s+language_version\s*(?:>=?|<=?|==|~)\s*([\d.]+)/);
881
+ const languageVersion = pragmaMatch ? pragmaMatch[1] : null;
882
+ // Extract imports
883
+ const imports = [];
884
+ const importMatches = code.matchAll(/import\s+(\w+)|include\s+"([^"]+)"/g);
885
+ for (const match of importMatches) {
886
+ imports.push(match[1] || match[2]);
887
+ }
888
+ // Extract exported circuits
889
+ const circuits = [];
890
+ // Helper to split parameters handling nested angle brackets, square brackets, parentheses,
891
+ // and string literals (e.g., Map<A, B>, [Field, Boolean], (x: Field) => Boolean, Opaque<"a, b">)
892
+ const splitParams = (paramsStr) => {
893
+ const result = [];
894
+ let current = "";
895
+ let angleDepth = 0;
896
+ let squareDepth = 0;
897
+ let parenDepth = 0;
898
+ let inString = false;
899
+ let stringChar = "";
900
+ for (let i = 0; i < paramsStr.length; i++) {
901
+ const ch = paramsStr[i];
902
+ // Handle string literals
903
+ if ((ch === '"' || ch === "'") &&
904
+ (i === 0 || paramsStr[i - 1] !== "\\")) {
905
+ if (!inString) {
906
+ inString = true;
907
+ stringChar = ch;
908
+ }
909
+ else if (ch === stringChar) {
910
+ inString = false;
911
+ stringChar = "";
912
+ }
913
+ }
914
+ // Only track depth when not inside a string
915
+ if (!inString) {
916
+ if (ch === "<")
917
+ angleDepth++;
918
+ else if (ch === ">")
919
+ angleDepth = Math.max(0, angleDepth - 1);
920
+ else if (ch === "[")
921
+ squareDepth++;
922
+ else if (ch === "]")
923
+ squareDepth = Math.max(0, squareDepth - 1);
924
+ else if (ch === "(")
925
+ parenDepth++;
926
+ else if (ch === ")")
927
+ parenDepth = Math.max(0, parenDepth - 1);
928
+ }
929
+ if (ch === "," &&
930
+ !inString &&
931
+ angleDepth === 0 &&
932
+ squareDepth === 0 &&
933
+ parenDepth === 0) {
934
+ if (current.trim())
935
+ result.push(current.trim());
936
+ current = "";
937
+ }
938
+ else {
939
+ current += ch;
940
+ }
941
+ }
942
+ if (current.trim())
943
+ result.push(current.trim());
944
+ return result;
945
+ };
946
+ // Use a more permissive pattern for return types to handle complex nested types
947
+ // Note: [^)]* doesn't work for nested parens, so we use a manual extraction approach
948
+ const circuitStartPattern = /(?:(export)\s+)?circuit\s+(\w+)\s*\(/g;
949
+ const lines = code.split("\n");
950
+ // Precompute a mapping from character index to 1-based line number to avoid
951
+ // repeatedly scanning from the start of the string for each match.
952
+ const lineByIndex = new Array(code.length);
953
+ {
954
+ let currentLine = 1;
955
+ for (let i = 0; i < code.length; i++) {
956
+ lineByIndex[i] = currentLine;
957
+ if (code[i] === "\n") {
958
+ currentLine++;
959
+ }
960
+ }
961
+ }
962
+ let circuitMatch;
963
+ while ((circuitMatch = circuitStartPattern.exec(code)) !== null) {
964
+ const lineNum = lineByIndex[circuitMatch.index];
965
+ const isExport = circuitMatch[1] === "export";
966
+ const name = circuitMatch[2];
967
+ // Manually extract params by finding matching closing parenthesis
968
+ const startIdx = circuitMatch.index + circuitMatch[0].length;
969
+ let depth = 1;
970
+ let endIdx = startIdx;
971
+ while (endIdx < code.length && depth > 0) {
972
+ if (code[endIdx] === "(")
973
+ depth++;
974
+ else if (code[endIdx] === ")")
975
+ depth--;
976
+ endIdx++;
977
+ }
978
+ const paramsStr = code.substring(startIdx, endIdx - 1);
979
+ const params = splitParams(paramsStr);
980
+ // Extract return type after ): until { or newline or ;
981
+ const afterParams = code.substring(endIdx);
982
+ const returnTypeMatch = afterParams.match(/^\s*:\s*([^{\n;]+)/);
983
+ const returnType = returnTypeMatch ? returnTypeMatch[1].trim() : "[]";
984
+ circuits.push({
985
+ name,
986
+ params,
987
+ returnType,
988
+ isExport,
989
+ line: lineNum,
990
+ });
991
+ }
992
+ // Extract witnesses
993
+ const witnesses = [];
994
+ const witnessPattern = /(?:(export)\s+)?witness\s+(\w+)\s*:\s*([^;]+)/g;
995
+ let witnessMatch;
996
+ while ((witnessMatch = witnessPattern.exec(code)) !== null) {
997
+ const lineNum = lineByIndex[witnessMatch.index];
998
+ witnesses.push({
999
+ name: witnessMatch[2],
1000
+ type: witnessMatch[3].trim(),
1001
+ isExport: witnessMatch[1] === "export",
1002
+ line: lineNum,
1003
+ });
1004
+ }
1005
+ // Extract ledger items
1006
+ const ledgerItems = [];
1007
+ const ledgerPattern = /(?:(export)\s+)?ledger\s+(\w+)\s*:\s*([^;]+)/g;
1008
+ let ledgerMatch;
1009
+ while ((ledgerMatch = ledgerPattern.exec(code)) !== null) {
1010
+ const lineNum = lineByIndex[ledgerMatch.index];
1011
+ ledgerItems.push({
1012
+ name: ledgerMatch[2],
1013
+ type: ledgerMatch[3].trim(),
1014
+ isExport: ledgerMatch[1] === "export",
1015
+ line: lineNum,
1016
+ });
1017
+ }
1018
+ // Extract type definitions
1019
+ const types = [];
1020
+ const typePattern = /type\s+(\w+)\s*=\s*([^;]+)/g;
1021
+ let typeMatch;
1022
+ while ((typeMatch = typePattern.exec(code)) !== null) {
1023
+ const lineNum = lineByIndex[typeMatch.index];
1024
+ types.push({
1025
+ name: typeMatch[1],
1026
+ definition: typeMatch[2].trim(),
1027
+ line: lineNum,
1028
+ });
1029
+ }
1030
+ // Extract struct definitions
1031
+ const structs = [];
1032
+ /**
1033
+ * Extract the contents of a balanced brace block starting at `startIndex`,
1034
+ * handling nested braces and skipping over comments and string literals.
1035
+ */
1036
+ function extractBalancedBlock(source, startIndex) {
1037
+ let depth = 0;
1038
+ const length = source.length;
1039
+ let i = startIndex;
1040
+ if (source[i] !== "{") {
1041
+ return null;
1042
+ }
1043
+ depth = 1;
1044
+ i++;
1045
+ const bodyStart = i;
1046
+ while (i < length && depth > 0) {
1047
+ const ch = source[i];
1048
+ const next = i + 1 < length ? source[i + 1] : "";
1049
+ // Handle string literals and template literals
1050
+ if (ch === '"' || ch === "'" || ch === "`") {
1051
+ const quote = ch;
1052
+ i++;
1053
+ while (i < length) {
1054
+ const c = source[i];
1055
+ if (c === "\\" && i + 1 < length) {
1056
+ // Skip escaped character
1057
+ i += 2;
1058
+ continue;
1059
+ }
1060
+ if (c === quote) {
1061
+ i++;
1062
+ break;
1063
+ }
1064
+ i++;
1065
+ }
1066
+ continue;
1067
+ }
1068
+ // Handle line comments
1069
+ if (ch === "/" && next === "/") {
1070
+ i += 2;
1071
+ while (i < length && source[i] !== "\n") {
1072
+ i++;
1073
+ }
1074
+ continue;
1075
+ }
1076
+ // Handle block comments
1077
+ if (ch === "/" && next === "*") {
1078
+ i += 2;
1079
+ while (i < length &&
1080
+ !(source[i] === "*" && i + 1 < length && source[i + 1] === "/")) {
1081
+ i++;
1082
+ }
1083
+ if (i < length) {
1084
+ i += 2; // Skip closing */
1085
+ }
1086
+ continue;
1087
+ }
1088
+ if (ch === "{") {
1089
+ depth++;
1090
+ i++;
1091
+ continue;
1092
+ }
1093
+ if (ch === "}") {
1094
+ depth--;
1095
+ i++;
1096
+ if (depth === 0) {
1097
+ const body = source.slice(bodyStart, i - 1);
1098
+ return { body, endIndex: i - 1 };
1099
+ }
1100
+ continue;
1101
+ }
1102
+ i++;
1103
+ }
1104
+ return null;
1105
+ }
1106
+ const structPattern = /struct\s+(\w+)\s*\{/g;
1107
+ let structMatch;
1108
+ while ((structMatch = structPattern.exec(code)) !== null) {
1109
+ const lineNum = lineByIndex[structMatch.index];
1110
+ const openingBraceIndex = code.indexOf("{", structMatch.index);
1111
+ if (openingBraceIndex === -1) {
1112
+ continue;
1113
+ }
1114
+ const block = extractBalancedBlock(code, openingBraceIndex);
1115
+ if (!block) {
1116
+ continue;
1117
+ }
1118
+ const fields = block.body
1119
+ .split(",")
1120
+ .map((f) => f.trim())
1121
+ .filter((f) => f);
1122
+ structs.push({
1123
+ name: structMatch[1],
1124
+ fields,
1125
+ line: lineNum,
1126
+ });
1127
+ }
1128
+ // Extract enum definitions using balanced block extraction
1129
+ // (handles nested braces in comments/strings)
1130
+ const enums = [];
1131
+ const enumStartPattern = /enum\s+(\w+)\s*\{/g;
1132
+ let enumMatch;
1133
+ while ((enumMatch = enumStartPattern.exec(code)) !== null) {
1134
+ const lineNum = lineByIndex[enumMatch.index];
1135
+ const openingBraceIndex = code.indexOf("{", enumMatch.index);
1136
+ if (openingBraceIndex === -1) {
1137
+ continue;
1138
+ }
1139
+ const block = extractBalancedBlock(code, openingBraceIndex);
1140
+ if (!block) {
1141
+ continue;
1142
+ }
1143
+ const variants = block.body
1144
+ .split(",")
1145
+ .map((v) => v.trim())
1146
+ .filter((v) => v);
1147
+ enums.push({
1148
+ name: enumMatch[1],
1149
+ variants,
1150
+ line: lineNum,
1151
+ });
1152
+ }
1153
+ // Generate summary
1154
+ const exports = {
1155
+ circuits: circuits.filter((c) => c.isExport).map((c) => c.name),
1156
+ witnesses: witnesses.filter((w) => w.isExport).map((w) => w.name),
1157
+ ledger: ledgerItems.filter((l) => l.isExport).map((l) => l.name),
1158
+ };
1159
+ const summary = [];
1160
+ if (circuits.length > 0) {
1161
+ summary.push(`${circuits.length} circuit(s)`);
1162
+ }
1163
+ if (witnesses.length > 0) {
1164
+ summary.push(`${witnesses.length} witness(es)`);
1165
+ }
1166
+ if (ledgerItems.length > 0) {
1167
+ summary.push(`${ledgerItems.length} ledger item(s)`);
1168
+ }
1169
+ if (types.length > 0) {
1170
+ summary.push(`${types.length} type alias(es)`);
1171
+ }
1172
+ if (structs.length > 0) {
1173
+ summary.push(`${structs.length} struct(s)`);
1174
+ }
1175
+ if (enums.length > 0) {
1176
+ summary.push(`${enums.length} enum(s)`);
1177
+ }
1178
+ return {
1179
+ success: true,
1180
+ filename,
1181
+ languageVersion,
1182
+ imports,
1183
+ structure: {
1184
+ circuits,
1185
+ witnesses,
1186
+ ledgerItems,
1187
+ types,
1188
+ structs,
1189
+ enums,
1190
+ },
1191
+ exports,
1192
+ stats: {
1193
+ lineCount: lines.length,
1194
+ circuitCount: circuits.length,
1195
+ witnessCount: witnesses.length,
1196
+ ledgerCount: ledgerItems.length,
1197
+ typeCount: types.length,
1198
+ structCount: structs.length,
1199
+ enumCount: enums.length,
1200
+ exportedCircuits: exports.circuits.length,
1201
+ exportedWitnesses: exports.witnesses.length,
1202
+ exportedLedger: exports.ledger.length,
1203
+ },
1204
+ summary: summary.length > 0 ? summary.join(", ") : "Empty contract",
1205
+ message: `📋 Contract contains: ${summary.join(", ") || "no definitions found"}`,
1206
+ };
1207
+ }
1208
+ //# sourceMappingURL=validation.js.map