midnight-mcp 0.1.40 → 0.2.1

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.
Files changed (100) hide show
  1. package/README.md +34 -0
  2. package/dist/bin.d.ts +1 -0
  3. package/dist/bin.js +10764 -0
  4. package/dist/index.d.ts +205 -3
  5. package/dist/index.js +10722 -15
  6. package/package.json +16 -6
  7. package/dist/config/compact-version.d.ts +0 -183
  8. package/dist/config/compact-version.js +0 -423
  9. package/dist/db/index.d.ts +0 -3
  10. package/dist/db/index.js +0 -2
  11. package/dist/db/vectorStore.d.ts +0 -69
  12. package/dist/db/vectorStore.js +0 -196
  13. package/dist/pipeline/embeddings.d.ts +0 -25
  14. package/dist/pipeline/embeddings.js +0 -103
  15. package/dist/pipeline/github.d.ts +0 -84
  16. package/dist/pipeline/github.js +0 -399
  17. package/dist/pipeline/index.d.ts +0 -11
  18. package/dist/pipeline/index.js +0 -6
  19. package/dist/pipeline/indexer.d.ts +0 -41
  20. package/dist/pipeline/indexer.js +0 -254
  21. package/dist/pipeline/parser.d.ts +0 -46
  22. package/dist/pipeline/parser.js +0 -436
  23. package/dist/pipeline/releases.d.ts +0 -112
  24. package/dist/pipeline/releases.js +0 -298
  25. package/dist/pipeline/repository.d.ts +0 -372
  26. package/dist/pipeline/repository.js +0 -520
  27. package/dist/prompts/index.d.ts +0 -3
  28. package/dist/prompts/index.js +0 -2
  29. package/dist/prompts/templates.d.ts +0 -26
  30. package/dist/prompts/templates.js +0 -443
  31. package/dist/resources/code.d.ts +0 -15
  32. package/dist/resources/code.js +0 -122
  33. package/dist/resources/content/code-content.d.ts +0 -6
  34. package/dist/resources/content/code-content.js +0 -802
  35. package/dist/resources/content/docs-content.d.ts +0 -14
  36. package/dist/resources/content/docs-content.js +0 -1202
  37. package/dist/resources/content/index.d.ts +0 -6
  38. package/dist/resources/content/index.js +0 -6
  39. package/dist/resources/docs.d.ts +0 -15
  40. package/dist/resources/docs.js +0 -98
  41. package/dist/resources/index.d.ts +0 -6
  42. package/dist/resources/index.js +0 -13
  43. package/dist/resources/schemas.d.ts +0 -16
  44. package/dist/resources/schemas.js +0 -407
  45. package/dist/scripts/index-repos.d.ts +0 -12
  46. package/dist/scripts/index-repos.js +0 -53
  47. package/dist/server.d.ts +0 -43
  48. package/dist/server.js +0 -693
  49. package/dist/services/index.d.ts +0 -6
  50. package/dist/services/index.js +0 -6
  51. package/dist/services/sampling.d.ts +0 -62
  52. package/dist/services/sampling.js +0 -277
  53. package/dist/tools/analyze.d.ts +0 -106
  54. package/dist/tools/analyze.js +0 -431
  55. package/dist/tools/generation.d.ts +0 -9
  56. package/dist/tools/generation.js +0 -285
  57. package/dist/tools/health.d.ts +0 -120
  58. package/dist/tools/health.js +0 -362
  59. package/dist/tools/index.d.ts +0 -14
  60. package/dist/tools/index.js +0 -22
  61. package/dist/tools/meta.d.ts +0 -61
  62. package/dist/tools/meta.js +0 -282
  63. package/dist/tools/repository/constants.d.ts +0 -19
  64. package/dist/tools/repository/constants.js +0 -324
  65. package/dist/tools/repository/handlers.d.ts +0 -373
  66. package/dist/tools/repository/handlers.js +0 -724
  67. package/dist/tools/repository/index.d.ts +0 -9
  68. package/dist/tools/repository/index.js +0 -13
  69. package/dist/tools/repository/schemas.d.ts +0 -153
  70. package/dist/tools/repository/schemas.js +0 -106
  71. package/dist/tools/repository/tools.d.ts +0 -7
  72. package/dist/tools/repository/tools.js +0 -484
  73. package/dist/tools/repository/validation.d.ts +0 -106
  74. package/dist/tools/repository/validation.js +0 -820
  75. package/dist/tools/repository.d.ts +0 -6
  76. package/dist/tools/repository.js +0 -7
  77. package/dist/tools/search.d.ts +0 -76
  78. package/dist/tools/search.js +0 -423
  79. package/dist/types/index.d.ts +0 -2
  80. package/dist/types/index.js +0 -2
  81. package/dist/types/mcp.d.ts +0 -187
  82. package/dist/types/mcp.js +0 -6
  83. package/dist/utils/cache.d.ts +0 -77
  84. package/dist/utils/cache.js +0 -172
  85. package/dist/utils/config.d.ts +0 -70
  86. package/dist/utils/config.js +0 -294
  87. package/dist/utils/errors.d.ts +0 -111
  88. package/dist/utils/errors.js +0 -165
  89. package/dist/utils/health.d.ts +0 -29
  90. package/dist/utils/health.js +0 -132
  91. package/dist/utils/hosted-api.d.ts +0 -67
  92. package/dist/utils/hosted-api.js +0 -119
  93. package/dist/utils/index.d.ts +0 -16
  94. package/dist/utils/index.js +0 -15
  95. package/dist/utils/logger.d.ts +0 -48
  96. package/dist/utils/logger.js +0 -124
  97. package/dist/utils/rate-limit.d.ts +0 -61
  98. package/dist/utils/rate-limit.js +0 -148
  99. package/dist/utils/validation.d.ts +0 -52
  100. package/dist/utils/validation.js +0 -255
@@ -1,820 +0,0 @@
1
- /**
2
- * Contract validation handlers
3
- * Static analysis for Compact contracts
4
- */
5
- import { readFile } from "fs/promises";
6
- import { basename, isAbsolute, resolve } from "path";
7
- import { platform } from "process";
8
- import { logger } from "../../utils/index.js";
9
- // ============================================================================
10
- // SECURITY & VALIDATION HELPERS
11
- // ============================================================================
12
- /**
13
- * Validate file path for security - prevent path traversal attacks
14
- */
15
- function validateFilePath(filePath) {
16
- // Must be absolute path
17
- if (!isAbsolute(filePath)) {
18
- return {
19
- valid: false,
20
- error: "File path must be absolute (e.g., /Users/you/contract.compact)",
21
- };
22
- }
23
- // Resolve to catch ../ traversal
24
- const normalized = resolve(filePath);
25
- // Check for path traversal attempts
26
- // Simply check for ".." in the path - this is always suspicious in absolute paths
27
- if (filePath.includes("..")) {
28
- return {
29
- valid: false,
30
- error: "Path traversal detected - use absolute paths without ../",
31
- };
32
- }
33
- // Must end with .compact
34
- if (!normalized.endsWith(".compact")) {
35
- return {
36
- valid: false,
37
- error: "File must have .compact extension",
38
- };
39
- }
40
- // Block sensitive paths (Unix and Windows)
41
- const blockedPathsUnix = ["/etc", "/var", "/usr", "/bin", "/sbin", "/root"];
42
- const blockedPathsWindows = [
43
- "C:\\Windows",
44
- "C:\\Program Files",
45
- "C:\\Program Files (x86)",
46
- "C:\\System32",
47
- "C:\\ProgramData",
48
- ];
49
- const blockedPaths = platform === "win32" ? blockedPathsWindows : blockedPathsUnix;
50
- const normalizedLower = normalized.toLowerCase();
51
- if (blockedPaths.some((blocked) => normalizedLower.startsWith(blocked.toLowerCase()))) {
52
- return {
53
- valid: false,
54
- error: "Cannot access system directories",
55
- };
56
- }
57
- return { valid: true, normalizedPath: normalized };
58
- }
59
- /**
60
- * Check if content is valid UTF-8 text (not binary)
61
- */
62
- function isValidUtf8Text(content) {
63
- // Check for null bytes (common in binary files)
64
- if (content.includes("\x00")) {
65
- return false;
66
- }
67
- // Check for excessive non-printable characters
68
- const nonPrintable = content.match(/[\x00-\x08\x0B\x0C\x0E-\x1F]/g);
69
- if (nonPrintable && nonPrintable.length > content.length * 0.01) {
70
- return false;
71
- }
72
- return true;
73
- }
74
- // ============================================================================
75
- // CONTRACT STRUCTURE EXTRACTION
76
- // ============================================================================
77
- /**
78
- * Extract the structure of a Compact contract (circuits, witnesses, ledger, etc.)
79
- * This helps agents understand what a contract does without parsing it themselves
80
- */
81
- export async function extractContractStructure(input) {
82
- logger.debug("Extracting contract structure", {
83
- hasCode: !!input.code,
84
- filePath: input.filePath,
85
- });
86
- // Resolve code source
87
- let code;
88
- let filename;
89
- if (input.filePath) {
90
- // SECURITY: Validate file path
91
- const pathValidation = validateFilePath(input.filePath);
92
- if (!pathValidation.valid) {
93
- return {
94
- success: false,
95
- error: "Invalid file path",
96
- message: pathValidation.error,
97
- };
98
- }
99
- try {
100
- code = await readFile(pathValidation.normalizedPath, "utf-8");
101
- filename = basename(pathValidation.normalizedPath);
102
- // Check for binary content
103
- if (!isValidUtf8Text(code)) {
104
- return {
105
- success: false,
106
- error: "Invalid file content",
107
- message: "File appears to be binary or contains invalid characters",
108
- };
109
- }
110
- }
111
- catch (fsError) {
112
- const err = fsError;
113
- return {
114
- success: false,
115
- error: "Failed to read file",
116
- message: `Cannot read file: ${input.filePath}`,
117
- details: err.code === "ENOENT" ? "File does not exist" : err.message,
118
- };
119
- }
120
- }
121
- else if (input.code) {
122
- code = input.code;
123
- filename = "contract.compact";
124
- // Check for binary content
125
- if (!isValidUtf8Text(code)) {
126
- return {
127
- success: false,
128
- error: "Invalid code content",
129
- message: "Code contains invalid characters",
130
- };
131
- }
132
- }
133
- else {
134
- return {
135
- success: false,
136
- error: "No contract provided",
137
- message: "Must provide either 'code' or 'filePath'",
138
- };
139
- }
140
- // Extract pragma version (supports >=, >, <=, <, ==, ~; >=? and <=? are ordered
141
- // so that >= and <= are matched before > and <)
142
- const pragmaMatch = code.match(/pragma\s+language_version\s*(?:>=?|<=?|==|~)\s*([\d.]+)/);
143
- const languageVersion = pragmaMatch ? pragmaMatch[1] : null;
144
- // Extract imports
145
- const imports = [];
146
- const importMatches = code.matchAll(/import\s+(\w+)|include\s+"([^"]+)"/g);
147
- for (const match of importMatches) {
148
- imports.push(match[1] || match[2]);
149
- }
150
- // Extract exported circuits
151
- const circuits = [];
152
- // Helper to split parameters handling nested angle brackets, square brackets, parentheses,
153
- // and string literals (e.g., Map<A, B>, [Field, Boolean], (x: Field) => Boolean, Opaque<"a, b">)
154
- const splitParams = (paramsStr) => {
155
- const result = [];
156
- let current = "";
157
- let angleDepth = 0;
158
- let squareDepth = 0;
159
- let parenDepth = 0;
160
- let inString = false;
161
- let stringChar = "";
162
- for (let i = 0; i < paramsStr.length; i++) {
163
- const ch = paramsStr[i];
164
- // Handle string literals
165
- if ((ch === '"' || ch === "'") &&
166
- (i === 0 || paramsStr[i - 1] !== "\\")) {
167
- if (!inString) {
168
- inString = true;
169
- stringChar = ch;
170
- }
171
- else if (ch === stringChar) {
172
- inString = false;
173
- stringChar = "";
174
- }
175
- }
176
- // Only track depth when not inside a string
177
- if (!inString) {
178
- if (ch === "<")
179
- angleDepth++;
180
- else if (ch === ">")
181
- angleDepth = Math.max(0, angleDepth - 1);
182
- else if (ch === "[")
183
- squareDepth++;
184
- else if (ch === "]")
185
- squareDepth = Math.max(0, squareDepth - 1);
186
- else if (ch === "(")
187
- parenDepth++;
188
- else if (ch === ")")
189
- parenDepth = Math.max(0, parenDepth - 1);
190
- }
191
- if (ch === "," &&
192
- !inString &&
193
- angleDepth === 0 &&
194
- squareDepth === 0 &&
195
- parenDepth === 0) {
196
- if (current.trim())
197
- result.push(current.trim());
198
- current = "";
199
- }
200
- else {
201
- current += ch;
202
- }
203
- }
204
- if (current.trim())
205
- result.push(current.trim());
206
- return result;
207
- };
208
- // Use a more permissive pattern for return types to handle complex nested types
209
- // Note: [^)]* doesn't work for nested parens, so we use a manual extraction approach
210
- const circuitStartPattern = /(?:(export)\s+)?circuit\s+(\w+)\s*\(/g;
211
- const lines = code.split("\n");
212
- // Precompute a mapping from character index to 1-based line number to avoid
213
- // repeatedly scanning from the start of the string for each match.
214
- const lineByIndex = new Array(code.length);
215
- {
216
- let currentLine = 1;
217
- for (let i = 0; i < code.length; i++) {
218
- lineByIndex[i] = currentLine;
219
- if (code[i] === "\n") {
220
- currentLine++;
221
- }
222
- }
223
- }
224
- let circuitMatch;
225
- while ((circuitMatch = circuitStartPattern.exec(code)) !== null) {
226
- const lineNum = lineByIndex[circuitMatch.index];
227
- const isExport = circuitMatch[1] === "export";
228
- const name = circuitMatch[2];
229
- // Manually extract params by finding matching closing parenthesis
230
- const startIdx = circuitMatch.index + circuitMatch[0].length;
231
- let depth = 1;
232
- let endIdx = startIdx;
233
- while (endIdx < code.length && depth > 0) {
234
- if (code[endIdx] === "(")
235
- depth++;
236
- else if (code[endIdx] === ")")
237
- depth--;
238
- endIdx++;
239
- }
240
- const paramsStr = code.substring(startIdx, endIdx - 1);
241
- const params = splitParams(paramsStr);
242
- // Extract return type after ): until { or newline or ;
243
- const afterParams = code.substring(endIdx);
244
- const returnTypeMatch = afterParams.match(/^\s*:\s*([^{\n;]+)/);
245
- const returnType = returnTypeMatch ? returnTypeMatch[1].trim() : "[]";
246
- circuits.push({
247
- name,
248
- params,
249
- returnType,
250
- isExport,
251
- line: lineNum,
252
- });
253
- }
254
- // Extract witnesses
255
- const witnesses = [];
256
- const witnessPattern = /(?:(export)\s+)?witness\s+(\w+)\s*:\s*([^;]+)/g;
257
- let witnessMatch;
258
- while ((witnessMatch = witnessPattern.exec(code)) !== null) {
259
- const lineNum = lineByIndex[witnessMatch.index];
260
- witnesses.push({
261
- name: witnessMatch[2],
262
- type: witnessMatch[3].trim(),
263
- isExport: witnessMatch[1] === "export",
264
- line: lineNum,
265
- });
266
- }
267
- // Extract ledger items
268
- const ledgerItems = [];
269
- const ledgerPattern = /(?:(export)\s+)?ledger\s+(\w+)\s*:\s*([^;]+)/g;
270
- let ledgerMatch;
271
- while ((ledgerMatch = ledgerPattern.exec(code)) !== null) {
272
- const lineNum = lineByIndex[ledgerMatch.index];
273
- ledgerItems.push({
274
- name: ledgerMatch[2],
275
- type: ledgerMatch[3].trim(),
276
- isExport: ledgerMatch[1] === "export",
277
- line: lineNum,
278
- });
279
- }
280
- // Extract type definitions
281
- const types = [];
282
- const typePattern = /type\s+(\w+)\s*=\s*([^;]+)/g;
283
- let typeMatch;
284
- while ((typeMatch = typePattern.exec(code)) !== null) {
285
- const lineNum = lineByIndex[typeMatch.index];
286
- types.push({
287
- name: typeMatch[1],
288
- definition: typeMatch[2].trim(),
289
- line: lineNum,
290
- });
291
- }
292
- // Extract struct definitions
293
- const structs = [];
294
- /**
295
- * Extract the contents of a balanced brace block starting at `startIndex`,
296
- * handling nested braces and skipping over comments and string literals.
297
- */
298
- function extractBalancedBlock(source, startIndex) {
299
- let depth = 0;
300
- const length = source.length;
301
- let i = startIndex;
302
- if (source[i] !== "{") {
303
- return null;
304
- }
305
- depth = 1;
306
- i++;
307
- const bodyStart = i;
308
- while (i < length && depth > 0) {
309
- const ch = source[i];
310
- const next = i + 1 < length ? source[i + 1] : "";
311
- // Handle string literals and template literals
312
- if (ch === '"' || ch === "'" || ch === "`") {
313
- const quote = ch;
314
- i++;
315
- while (i < length) {
316
- const c = source[i];
317
- if (c === "\\" && i + 1 < length) {
318
- // Skip escaped character
319
- i += 2;
320
- continue;
321
- }
322
- if (c === quote) {
323
- i++;
324
- break;
325
- }
326
- i++;
327
- }
328
- continue;
329
- }
330
- // Handle line comments
331
- if (ch === "/" && next === "/") {
332
- i += 2;
333
- while (i < length && source[i] !== "\n") {
334
- i++;
335
- }
336
- continue;
337
- }
338
- // Handle block comments
339
- if (ch === "/" && next === "*") {
340
- i += 2;
341
- while (i < length &&
342
- !(source[i] === "*" && i + 1 < length && source[i + 1] === "/")) {
343
- i++;
344
- }
345
- if (i < length) {
346
- i += 2; // Skip closing */
347
- }
348
- continue;
349
- }
350
- if (ch === "{") {
351
- depth++;
352
- i++;
353
- continue;
354
- }
355
- if (ch === "}") {
356
- depth--;
357
- i++;
358
- if (depth === 0) {
359
- const body = source.slice(bodyStart, i - 1);
360
- return { body, endIndex: i - 1 };
361
- }
362
- continue;
363
- }
364
- i++;
365
- }
366
- return null;
367
- }
368
- const structPattern = /struct\s+(\w+)\s*\{/g;
369
- let structMatch;
370
- while ((structMatch = structPattern.exec(code)) !== null) {
371
- const lineNum = lineByIndex[structMatch.index];
372
- const openingBraceIndex = code.indexOf("{", structMatch.index);
373
- if (openingBraceIndex === -1) {
374
- continue;
375
- }
376
- const block = extractBalancedBlock(code, openingBraceIndex);
377
- if (!block) {
378
- continue;
379
- }
380
- const fields = block.body
381
- .split(",")
382
- .map((f) => f.trim())
383
- .filter((f) => f);
384
- structs.push({
385
- name: structMatch[1],
386
- fields,
387
- line: lineNum,
388
- });
389
- }
390
- // Extract enum definitions using balanced block extraction
391
- // (handles nested braces in comments/strings)
392
- const enums = [];
393
- const enumStartPattern = /enum\s+(\w+)\s*\{/g;
394
- let enumMatch;
395
- while ((enumMatch = enumStartPattern.exec(code)) !== null) {
396
- const lineNum = lineByIndex[enumMatch.index];
397
- const openingBraceIndex = code.indexOf("{", enumMatch.index);
398
- if (openingBraceIndex === -1) {
399
- continue;
400
- }
401
- const block = extractBalancedBlock(code, openingBraceIndex);
402
- if (!block) {
403
- continue;
404
- }
405
- const variants = block.body
406
- .split(",")
407
- .map((v) => v.trim())
408
- .filter((v) => v);
409
- enums.push({
410
- name: enumMatch[1],
411
- variants,
412
- line: lineNum,
413
- });
414
- }
415
- // Generate summary
416
- const exports = {
417
- circuits: circuits.filter((c) => c.isExport).map((c) => c.name),
418
- witnesses: witnesses.filter((w) => w.isExport).map((w) => w.name),
419
- ledger: ledgerItems.filter((l) => l.isExport).map((l) => l.name),
420
- };
421
- // ============================================================================
422
- // PRE-COMPILATION ISSUE DETECTION
423
- // Catch common mistakes before hitting the compiler
424
- // ============================================================================
425
- const potentialIssues = [];
426
- // Known CompactStandardLibrary exports that shouldn't be redefined
427
- const stdlibExports = [
428
- "burnAddress",
429
- "ownPublicKey",
430
- "contractAddress",
431
- "default",
432
- "disclose",
433
- "assert",
434
- "pad",
435
- "unpad",
436
- "Counter",
437
- "Map",
438
- "Set",
439
- "MerkleTree",
440
- "Opaque",
441
- "Vector",
442
- ];
443
- // ========== CRITICAL SYNTAX CHECKS (P0 - causes immediate compilation failure) ==========
444
- // P0-1. Detect deprecated ledger block syntax
445
- const ledgerBlockPattern = /ledger\s*\{/g;
446
- let ledgerBlockMatch;
447
- while ((ledgerBlockMatch = ledgerBlockPattern.exec(code)) !== null) {
448
- const lineNum = lineByIndex[ledgerBlockMatch.index] || 1;
449
- potentialIssues.push({
450
- type: "deprecated_ledger_block",
451
- line: lineNum,
452
- message: `Deprecated ledger block syntax 'ledger { }' - causes parse error`,
453
- suggestion: `Use individual declarations: 'export ledger fieldName: Type;'`,
454
- severity: "error",
455
- });
456
- }
457
- // P0-2. Detect Void return type (doesn't exist in Compact)
458
- const voidReturnPattern = /circuit\s+\w+\s*\([^)]*\)\s*:\s*Void\b/g;
459
- let voidMatch;
460
- while ((voidMatch = voidReturnPattern.exec(code)) !== null) {
461
- const lineNum = lineByIndex[voidMatch.index] || 1;
462
- potentialIssues.push({
463
- type: "invalid_void_type",
464
- line: lineNum,
465
- message: `Invalid return type 'Void' - Void does not exist in Compact`,
466
- suggestion: `Use '[]' (empty tuple) for circuits that return nothing: 'circuit fn(): []'`,
467
- severity: "error",
468
- });
469
- }
470
- // P0-3. Detect old pragma format with patch version
471
- const oldPragmaPattern = /pragma\s+language_version\s*>=?\s*\d+\.\d+\.\d+/g;
472
- let oldPragmaMatch;
473
- while ((oldPragmaMatch = oldPragmaPattern.exec(code)) !== null) {
474
- const lineNum = lineByIndex[oldPragmaMatch.index] || 1;
475
- potentialIssues.push({
476
- type: "invalid_pragma_format",
477
- line: lineNum,
478
- message: `Pragma includes patch version which may cause parse errors`,
479
- suggestion: `Use bounded range format: 'pragma language_version >= 0.16 && <= 0.18;'`,
480
- severity: "error",
481
- });
482
- }
483
- // P0-4. Detect missing export on enums (won't be accessible from TypeScript)
484
- const unexportedEnumPattern = /(?<!export\s+)enum\s+(\w+)\s*\{/g;
485
- let unexportedEnumMatch;
486
- while ((unexportedEnumMatch = unexportedEnumPattern.exec(code)) !== null) {
487
- // Double check it's not preceded by export
488
- const before = code.substring(Math.max(0, unexportedEnumMatch.index - 10), unexportedEnumMatch.index);
489
- if (!before.includes("export")) {
490
- const lineNum = lineByIndex[unexportedEnumMatch.index] || 1;
491
- potentialIssues.push({
492
- type: "unexported_enum",
493
- line: lineNum,
494
- message: `Enum '${unexportedEnumMatch[1]}' is not exported - won't be accessible from TypeScript`,
495
- suggestion: `Add 'export' keyword: 'export enum ${unexportedEnumMatch[1]} { ... }'`,
496
- severity: "warning",
497
- });
498
- }
499
- }
500
- // P0-5. Detect Cell<T> wrapper (deprecated since 0.15)
501
- const cellPattern = /Cell\s*<\s*\w+\s*>/g;
502
- let cellMatch;
503
- while ((cellMatch = cellPattern.exec(code)) !== null) {
504
- const lineNum = lineByIndex[cellMatch.index] || 1;
505
- potentialIssues.push({
506
- type: "deprecated_cell_wrapper",
507
- line: lineNum,
508
- message: `'Cell<T>' wrapper is deprecated since Compact 0.15`,
509
- suggestion: `Use the type directly: 'Field' instead of 'Cell<Field>'`,
510
- severity: "error",
511
- });
512
- }
513
- // ========== EXISTING CHECKS ==========
514
- // 1. Detect module-level const (not supported in Compact)
515
- const constPattern = /^const\s+(\w+)\s*:/gm;
516
- let constMatch;
517
- while ((constMatch = constPattern.exec(code)) !== null) {
518
- // Check if this const is inside a circuit block by looking for preceding circuit/constructor
519
- const beforeConst = code.substring(0, constMatch.index);
520
- const lastCircuitStart = Math.max(beforeConst.lastIndexOf("circuit "), beforeConst.lastIndexOf("constructor {"));
521
- const lastCloseBrace = beforeConst.lastIndexOf("}");
522
- // If no circuit before, or the last } is after the last circuit start, it's module-level
523
- if (lastCircuitStart === -1 || lastCloseBrace > lastCircuitStart) {
524
- const lineNum = lineByIndex[constMatch.index] || 1;
525
- potentialIssues.push({
526
- type: "module_level_const",
527
- line: lineNum,
528
- message: `Module-level 'const ${constMatch[1]}' is not supported in Compact`,
529
- suggestion: `Use 'pure circuit ${constMatch[1]}(): <type> { return <value>; }' instead`,
530
- severity: "error",
531
- });
532
- }
533
- }
534
- // 2. Detect standard library name collisions
535
- const hasStdlibImport = imports.includes("CompactStandardLibrary") ||
536
- code.includes('include "std"');
537
- if (hasStdlibImport) {
538
- // Check circuits for name collisions
539
- for (const circuit of circuits) {
540
- if (stdlibExports.includes(circuit.name)) {
541
- potentialIssues.push({
542
- type: "stdlib_name_collision",
543
- line: circuit.line,
544
- message: `Circuit '${circuit.name}' conflicts with CompactStandardLibrary.${circuit.name}()`,
545
- suggestion: `Rename to avoid ambiguity, or remove to use the standard library version`,
546
- severity: "error",
547
- });
548
- }
549
- }
550
- }
551
- // 3. Detect sealed + export conflicts
552
- const sealedFields = [];
553
- const sealedPattern = /sealed\s+ledger\s+(\w+)\s*:/g;
554
- let sealedMatch;
555
- while ((sealedMatch = sealedPattern.exec(code)) !== null) {
556
- const lineNum = lineByIndex[sealedMatch.index] || 1;
557
- sealedFields.push({ name: sealedMatch[1], line: lineNum });
558
- }
559
- if (sealedFields.length > 0) {
560
- // Check if any exported circuit writes to sealed fields
561
- for (const circuit of circuits) {
562
- if (circuit.isExport) {
563
- // Find the circuit body and check for assignments to sealed fields
564
- const circuitBodyMatch = code.match(new RegExp(`(?:export\\s+)?circuit\\s+${circuit.name}\\s*\\([^)]*\\)\\s*:[^{]*\\{([\\s\\S]*?)\\n\\}`, "m"));
565
- if (circuitBodyMatch) {
566
- const body = circuitBodyMatch[1];
567
- for (const field of sealedFields) {
568
- // Check for assignment patterns: fieldName = or fieldName.method(
569
- if (new RegExp(`\\b${field.name}\\s*=`).test(body) ||
570
- new RegExp(`\\b${field.name}\\s*\\.\\s*\\w+\\s*\\(`).test(body)) {
571
- potentialIssues.push({
572
- type: "sealed_export_conflict",
573
- line: circuit.line,
574
- message: `Exported circuit '${circuit.name}' modifies sealed field '${field.name}'`,
575
- suggestion: `Move sealed field initialization to a 'constructor { }' block instead`,
576
- severity: "error",
577
- });
578
- }
579
- }
580
- }
581
- }
582
- }
583
- }
584
- // 4. Detect missing constructor when sealed fields exist but no constructor
585
- if (sealedFields.length > 0) {
586
- const hasConstructor = /constructor\s*\{/.test(code);
587
- if (!hasConstructor) {
588
- // Check if there's an initialize-like circuit trying to set sealed fields
589
- const initCircuit = circuits.find((c) => c.name.toLowerCase().includes("init") ||
590
- c.name.toLowerCase() === "setup");
591
- if (initCircuit && initCircuit.isExport) {
592
- potentialIssues.push({
593
- type: "missing_constructor",
594
- line: initCircuit.line,
595
- message: `Contract has sealed fields but uses '${initCircuit.name}' instead of constructor`,
596
- suggestion: `Sealed fields must be initialized in 'constructor { }', not in exported circuits`,
597
- severity: "warning",
598
- });
599
- }
600
- }
601
- }
602
- // 5. Detect potential type mismatches with stdlib functions
603
- if (hasStdlibImport) {
604
- // Check for burnAddress() used where ZswapCoinPublicKey is expected
605
- // burnAddress() returns Either<ZswapCoinPublicKey, ContractAddress>
606
- const burnAddressUsages = code.matchAll(/burnAddress\s*\(\s*\)/g);
607
- for (const usage of burnAddressUsages) {
608
- // Check if it's being passed to a function or assigned
609
- const afterUsage = code.substring(usage.index + usage[0].length, usage.index + usage[0].length + 50);
610
- const beforeUsage = code.substring(Math.max(0, usage.index - 100), usage.index);
611
- // If used in a context expecting ZswapCoinPublicKey (not .left or .right access)
612
- if (!afterUsage.startsWith(".left") &&
613
- !afterUsage.startsWith(".right") &&
614
- !afterUsage.startsWith(".is_left")) {
615
- // Check if it's in a function call or assignment that likely expects ZswapCoinPublicKey
616
- if (/\(\s*$/.test(beforeUsage) || /,\s*$/.test(beforeUsage)) {
617
- const lineNum = lineByIndex[usage.index] || 1;
618
- potentialIssues.push({
619
- type: "stdlib_type_mismatch",
620
- line: lineNum,
621
- message: `burnAddress() returns Either<ZswapCoinPublicKey, ContractAddress>, not ZswapCoinPublicKey`,
622
- suggestion: `Use burnAddress().left for ZswapCoinPublicKey, or define 'pure circuit zeroKey(): ZswapCoinPublicKey { return default<ZswapCoinPublicKey>; }'`,
623
- severity: "warning",
624
- });
625
- break; // Only warn once
626
- }
627
- }
628
- }
629
- }
630
- // 6. Detect division operator usage (not supported in Compact)
631
- const divisionPattern = /[^/]\/[^/*]/g;
632
- let divMatch;
633
- while ((divMatch = divisionPattern.exec(code)) !== null) {
634
- // Skip if inside a comment
635
- const beforeDiv = code.substring(0, divMatch.index);
636
- const lastLineStart = beforeDiv.lastIndexOf("\n") + 1;
637
- const lineContent = beforeDiv.substring(lastLineStart);
638
- if (lineContent.includes("//"))
639
- continue;
640
- const lineNum = lineByIndex[divMatch.index] || 1;
641
- potentialIssues.push({
642
- type: "unsupported_division",
643
- line: lineNum,
644
- message: `Division operator '/' is not supported in Compact`,
645
- suggestion: `Use a witness-based division pattern: 'witness divideWithRemainder(a, b): [quotient, remainder]' with on-chain verification`,
646
- severity: "error",
647
- });
648
- break; // Only warn once
649
- }
650
- // 7. Detect Counter.value access (Counter only has .increment())
651
- const counterValuePattern = /(\w+)\.value\b/g;
652
- let counterMatch;
653
- while ((counterMatch = counterValuePattern.exec(code)) !== null) {
654
- const varName = counterMatch[1];
655
- // Check if this variable is a Counter type
656
- const counterLedger = ledgerItems.find((l) => l.name === varName && l.type === "Counter");
657
- if (counterLedger) {
658
- const lineNum = lineByIndex[counterMatch.index] || 1;
659
- potentialIssues.push({
660
- type: "invalid_counter_access",
661
- line: lineNum,
662
- message: `Counter type '${varName}' does not have a '.value' property`,
663
- suggestion: `Counter only has '.increment(n)'. Use 'Uint<32>' or 'Uint<64>' instead if you need to read the value`,
664
- severity: "error",
665
- });
666
- }
667
- }
668
- // 8. Detect potential Uint overflow in multiplication (suggest Field casting)
669
- const multiplyPattern = /(\w+)\s*\*\s*(\w+)(?:\s*\+\s*\w+)?\s*(?:as\s+Uint|==)/g;
670
- let multMatch;
671
- while ((multMatch = multiplyPattern.exec(code)) !== null) {
672
- // Check if operands are likely Uint types and not already cast to Field
673
- const beforeMult = code.substring(Math.max(0, multMatch.index - 200), multMatch.index);
674
- const afterMult = code.substring(multMatch.index, multMatch.index + multMatch[0].length + 50);
675
- // Skip if already casting to Field
676
- if (afterMult.includes("as Field") || beforeMult.includes("as Field"))
677
- continue;
678
- // Check if this looks like a verification pattern (common in witness verification)
679
- if (/assert|==/.test(afterMult)) {
680
- const lineNum = lineByIndex[multMatch.index] || 1;
681
- potentialIssues.push({
682
- type: "potential_overflow",
683
- line: lineNum,
684
- message: `Multiplication '${multMatch[1]} * ${multMatch[2]}' may overflow Uint bounds`,
685
- suggestion: `Cast operands to Field for safe arithmetic: '(${multMatch[1]} as Field) * (${multMatch[2]} as Field)'`,
686
- severity: "warning",
687
- });
688
- break; // Only warn once
689
- }
690
- }
691
- // 9. Detect witness/private values used in conditionals without disclose()
692
- // Look for patterns like: if (witnessVar ...) or if (param == privateValue)
693
- const witnessNames = witnesses.map((w) => w.name);
694
- const ifPattern = /if\s*\(([^)]+)\)/g;
695
- let ifMatch;
696
- while ((ifMatch = ifPattern.exec(code)) !== null) {
697
- const condition = ifMatch[1];
698
- // Check if condition uses a witness value without disclose
699
- for (const witnessName of witnessNames) {
700
- if (condition.includes(witnessName) &&
701
- !condition.includes(`disclose(${witnessName}`) &&
702
- !condition.includes("disclose(")) {
703
- const lineNum = lineByIndex[ifMatch.index] || 1;
704
- potentialIssues.push({
705
- type: "undisclosed_witness_conditional",
706
- line: lineNum,
707
- message: `Witness value '${witnessName}' used in conditional without disclose()`,
708
- suggestion: `Wrap witness comparisons in disclose(): 'if (disclose(${witnessName} == expected))'`,
709
- severity: "warning",
710
- });
711
- break;
712
- }
713
- }
714
- }
715
- // 10. Detect constructor parameters assigned to ledger without disclose()
716
- // Constructor parameters are treated as witness values and need disclose() when written to ledger
717
- const constructorMatch = code.match(/constructor\s*\(([^)]*)\)\s*\{([\s\S]*?)(?=\n\s*(?:export|circuit|witness|ledger|constructor|\}|$))/);
718
- if (constructorMatch) {
719
- const paramsStr = constructorMatch[1];
720
- const constructorBody = constructorMatch[2];
721
- // Extract constructor parameter names
722
- const paramPattern = /(\w+)\s*:\s*[^,)]+/g;
723
- const constructorParams = [];
724
- let paramMatch;
725
- while ((paramMatch = paramPattern.exec(paramsStr)) !== null) {
726
- constructorParams.push(paramMatch[1]);
727
- }
728
- // Check each parameter for direct assignment to ledger without disclose
729
- for (const param of constructorParams) {
730
- // Look for direct assignment: ledgerField = param (without disclose)
731
- const assignmentPattern = new RegExp(`(\\w+)\\s*=\\s*(?!disclose\\s*\\()${param}\\b`, "g");
732
- let assignMatch;
733
- while ((assignMatch = assignmentPattern.exec(constructorBody)) !== null) {
734
- const fieldName = assignMatch[1];
735
- // Check if the field is a ledger item
736
- const isLedgerField = ledgerItems.some((l) => l.name === fieldName);
737
- if (isLedgerField) {
738
- // Find the line number
739
- const beforeAssign = code.substring(0, constructorMatch.index +
740
- constructorMatch[0].indexOf(assignMatch[0]));
741
- const lineNum = (beforeAssign.match(/\n/g) || []).length + 1;
742
- potentialIssues.push({
743
- type: "undisclosed_constructor_param",
744
- line: lineNum,
745
- message: `Constructor parameter '${param}' assigned to ledger field '${fieldName}' without disclose()`,
746
- suggestion: `Wrap in disclose(): '${fieldName} = disclose(${param});'`,
747
- severity: "error",
748
- });
749
- }
750
- }
751
- }
752
- }
753
- // 11. Detect "if" expression used in assignment context (should use ternary)
754
- // Pattern: const x = if (...) { ... } else { ... }
755
- const ifAssignmentPattern = /(?:const|let)\s+\w+\s*=\s*if\s*\(/g;
756
- let ifAssignMatch;
757
- while ((ifAssignMatch = ifAssignmentPattern.exec(code)) !== null) {
758
- const lineNum = lineByIndex[ifAssignMatch.index] || 1;
759
- potentialIssues.push({
760
- type: "invalid_if_expression",
761
- line: lineNum,
762
- message: `'if' cannot be used as an expression in assignments`,
763
- suggestion: `Use ternary operator instead: 'const x = condition ? valueIfTrue : valueIfFalse;'`,
764
- severity: "error",
765
- });
766
- }
767
- // 12. (Moved to P0-2 above - Void return type check)
768
- const summary = [];
769
- if (circuits.length > 0) {
770
- summary.push(`${circuits.length} circuit(s)`);
771
- }
772
- if (witnesses.length > 0) {
773
- summary.push(`${witnesses.length} witness(es)`);
774
- }
775
- if (ledgerItems.length > 0) {
776
- summary.push(`${ledgerItems.length} ledger item(s)`);
777
- }
778
- if (types.length > 0) {
779
- summary.push(`${types.length} type alias(es)`);
780
- }
781
- if (structs.length > 0) {
782
- summary.push(`${structs.length} struct(s)`);
783
- }
784
- if (enums.length > 0) {
785
- summary.push(`${enums.length} enum(s)`);
786
- }
787
- return {
788
- success: true,
789
- filename,
790
- languageVersion,
791
- imports,
792
- structure: {
793
- circuits,
794
- witnesses,
795
- ledgerItems,
796
- types,
797
- structs,
798
- enums,
799
- },
800
- exports,
801
- stats: {
802
- lineCount: lines.length,
803
- circuitCount: circuits.length,
804
- witnessCount: witnesses.length,
805
- ledgerCount: ledgerItems.length,
806
- typeCount: types.length,
807
- structCount: structs.length,
808
- enumCount: enums.length,
809
- exportedCircuits: exports.circuits.length,
810
- exportedWitnesses: exports.witnesses.length,
811
- exportedLedger: exports.ledger.length,
812
- },
813
- potentialIssues: potentialIssues.length > 0 ? potentialIssues : undefined,
814
- summary: summary.length > 0 ? summary.join(", ") : "Empty contract",
815
- message: potentialIssues.length > 0
816
- ? `⚠️ Found ${potentialIssues.length} potential issue(s). Contract contains: ${summary.join(", ") || "no definitions found"}`
817
- : `📋 Contract contains: ${summary.join(", ") || "no definitions found"}`,
818
- };
819
- }
820
- //# sourceMappingURL=validation.js.map