@schemashift/core 0.7.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -264,6 +264,465 @@ function transformMethodChain(chain, newBase, factoryMapper, methodMapper) {
264
264
  return buildCallChain(newBase, factory.name, factory.args, mappedMethods);
265
265
  }
266
266
 
267
+ // src/audit-log.ts
268
+ import { createHash } from "crypto";
269
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
270
+ import { join } from "path";
271
+ var AUDIT_DIR = ".schemashift";
272
+ var AUDIT_FILE = "audit-log.json";
273
+ var AUDIT_VERSION = 1;
274
+ var MigrationAuditLog = class {
275
+ logDir;
276
+ logPath;
277
+ constructor(projectPath) {
278
+ this.logDir = join(projectPath, AUDIT_DIR);
279
+ this.logPath = join(this.logDir, AUDIT_FILE);
280
+ }
281
+ /**
282
+ * Append a new entry to the audit log.
283
+ */
284
+ append(entry) {
285
+ const log = this.read();
286
+ log.entries.push(entry);
287
+ this.write(log);
288
+ }
289
+ /**
290
+ * Create an audit entry for a file transformation.
291
+ */
292
+ createEntry(params) {
293
+ return {
294
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
295
+ migrationId: params.migrationId,
296
+ filePath: params.filePath,
297
+ action: "transform",
298
+ from: params.from,
299
+ to: params.to,
300
+ success: params.success,
301
+ beforeHash: this.hashContent(params.originalCode),
302
+ afterHash: params.transformedCode ? this.hashContent(params.transformedCode) : void 0,
303
+ warningCount: params.warningCount,
304
+ errorCount: params.errorCount,
305
+ riskScore: params.riskScore,
306
+ duration: params.duration,
307
+ user: this.getCurrentUser()
308
+ };
309
+ }
310
+ /**
311
+ * Read the current audit log.
312
+ */
313
+ read() {
314
+ if (!existsSync(this.logPath)) {
315
+ return { version: AUDIT_VERSION, entries: [] };
316
+ }
317
+ try {
318
+ const content = readFileSync(this.logPath, "utf-8");
319
+ if (!content.trim()) {
320
+ return { version: AUDIT_VERSION, entries: [] };
321
+ }
322
+ return JSON.parse(content);
323
+ } catch {
324
+ return { version: AUDIT_VERSION, entries: [] };
325
+ }
326
+ }
327
+ /**
328
+ * Get entries for a specific migration.
329
+ */
330
+ getByMigration(migrationId) {
331
+ const log = this.read();
332
+ return log.entries.filter((e) => e.migrationId === migrationId);
333
+ }
334
+ /**
335
+ * Get summary statistics for the audit log.
336
+ */
337
+ getSummary() {
338
+ const log = this.read();
339
+ const migrationIds = new Set(log.entries.map((e) => e.migrationId));
340
+ const migrationPaths = [...new Set(log.entries.map((e) => `${e.from}->${e.to}`))];
341
+ return {
342
+ totalMigrations: migrationIds.size,
343
+ totalFiles: log.entries.length,
344
+ successCount: log.entries.filter((e) => e.success).length,
345
+ failureCount: log.entries.filter((e) => !e.success).length,
346
+ migrationPaths
347
+ };
348
+ }
349
+ /**
350
+ * Clear the audit log.
351
+ */
352
+ clear() {
353
+ this.write({ version: AUDIT_VERSION, entries: [] });
354
+ }
355
+ write(log) {
356
+ if (!existsSync(this.logDir)) {
357
+ mkdirSync(this.logDir, { recursive: true });
358
+ }
359
+ writeFileSync(this.logPath, JSON.stringify(log, null, 2));
360
+ }
361
+ hashContent(content) {
362
+ return createHash("sha256").update(content).digest("hex").substring(0, 16);
363
+ }
364
+ getCurrentUser() {
365
+ return process.env.USER || process.env.USERNAME || void 0;
366
+ }
367
+ };
368
+
369
+ // src/behavioral-warnings.ts
370
+ var BEHAVIORAL_RULES = [
371
+ // Yup -> Zod: Type coercion differences
372
+ {
373
+ category: "type-coercion",
374
+ migrations: ["yup->zod"],
375
+ detect: (text, filePath) => {
376
+ const warnings = [];
377
+ if (/yup\.(number|date)\s*\(\)/.test(text)) {
378
+ warnings.push({
379
+ category: "type-coercion",
380
+ message: "Yup silently coerces types; Zod rejects mismatches.",
381
+ detail: `Yup's number() accepts strings like "42" and coerces them. Zod's number() rejects strings. Use z.coerce.number() for equivalent behavior, especially for HTML form inputs which always return strings.`,
382
+ filePath,
383
+ severity: "warning",
384
+ migration: "yup->zod"
385
+ });
386
+ }
387
+ return warnings;
388
+ }
389
+ },
390
+ // Yup -> Zod: Form input string values
391
+ {
392
+ category: "form-input",
393
+ migrations: ["yup->zod"],
394
+ detect: (text, filePath) => {
395
+ const warnings = [];
396
+ const hasFormImport = /yupResolver|useFormik|from\s+['"]formik['"]|from\s+['"]@hookform/.test(
397
+ text
398
+ );
399
+ const hasNumberOrDate = /yup\.(number|date)\s*\(\)/.test(text);
400
+ if (hasFormImport && hasNumberOrDate) {
401
+ warnings.push({
402
+ category: "form-input",
403
+ message: "HTML inputs return strings \u2014 Zod will reject unless using z.coerce.*",
404
+ detail: 'HTML <input type="number"> returns strings. Yup coerces automatically, but Zod requires explicit coercion. Use z.coerce.number() or register({ valueAsNumber: true }) in React Hook Form.',
405
+ filePath,
406
+ severity: "error",
407
+ migration: "yup->zod"
408
+ });
409
+ }
410
+ return warnings;
411
+ }
412
+ },
413
+ // Joi -> Zod: Error handling paradigm shift
414
+ {
415
+ category: "error-handling",
416
+ migrations: ["joi->zod"],
417
+ detect: (text, filePath) => {
418
+ const warnings = [];
419
+ if (/\.validate\s*\(/.test(text) && /[Jj]oi/.test(text)) {
420
+ warnings.push({
421
+ category: "error-handling",
422
+ message: "Joi .validate() returns { value, error }; Zod .parse() throws.",
423
+ detail: "Joi uses an inspection pattern: .validate() returns an object with value and error. Zod .parse() throws a ZodError on failure. Use .safeParse() for a non-throwing equivalent that returns { success, data, error }.",
424
+ filePath,
425
+ severity: "warning",
426
+ migration: "joi->zod"
427
+ });
428
+ }
429
+ return warnings;
430
+ }
431
+ },
432
+ // Joi -> Zod: Null handling differences
433
+ {
434
+ category: "null-handling",
435
+ migrations: ["joi->zod"],
436
+ detect: (text, filePath) => {
437
+ const warnings = [];
438
+ if (/\.allow\s*\(\s*null\s*\)/.test(text)) {
439
+ warnings.push({
440
+ category: "null-handling",
441
+ message: "Joi .allow(null) vs Zod .nullable() have subtle differences.",
442
+ detail: 'Joi .allow(null) permits null alongside the base type. Zod .nullable() wraps the type in a union with null. Joi .allow("", null) has no single Zod equivalent \u2014 use z.union() or .transform().',
443
+ filePath,
444
+ severity: "info",
445
+ migration: "joi->zod"
446
+ });
447
+ }
448
+ return warnings;
449
+ }
450
+ },
451
+ // Zod v3 -> v4: Default value behavior change
452
+ {
453
+ category: "default-values",
454
+ migrations: ["zod-v3->v4"],
455
+ detect: (text, filePath) => {
456
+ const warnings = [];
457
+ if (/\.default\s*\(/.test(text) && /\.optional\s*\(\)/.test(text)) {
458
+ warnings.push({
459
+ category: "default-values",
460
+ message: ".default() + .optional() behavior changed silently in Zod v4.",
461
+ detail: "In Zod v3, .default(val).optional() returned undefined when property was missing. In Zod v4, it always returns the default value. This can cause unexpected behavior in API responses and form handling.",
462
+ filePath,
463
+ severity: "error",
464
+ migration: "zod-v3->v4"
465
+ });
466
+ }
467
+ if (/\.catch\s*\(/.test(text) && /\.optional\s*\(\)/.test(text)) {
468
+ warnings.push({
469
+ category: "default-values",
470
+ message: ".catch() + .optional() behavior changed in Zod v4.",
471
+ detail: "In Zod v4, object properties with .catch() that are .optional() now always return the caught value, even when the property is missing from input.",
472
+ filePath,
473
+ severity: "warning",
474
+ migration: "zod-v3->v4"
475
+ });
476
+ }
477
+ return warnings;
478
+ }
479
+ },
480
+ // Zod v3 -> v4: Error format differences
481
+ {
482
+ category: "error-format",
483
+ migrations: ["zod-v3->v4"],
484
+ detect: (text, filePath) => {
485
+ const warnings = [];
486
+ if (/ZodError/.test(text) && /instanceof\s+Error/.test(text)) {
487
+ warnings.push({
488
+ category: "error-format",
489
+ message: "ZodError no longer extends Error in Zod v4.",
490
+ detail: 'In Zod v4, ZodError no longer extends Error. Code using "instanceof Error" to catch ZodErrors will silently miss them. Use "instanceof ZodError" or z.isZodError() instead.',
491
+ filePath,
492
+ severity: "error",
493
+ migration: "zod-v3->v4"
494
+ });
495
+ }
496
+ return warnings;
497
+ }
498
+ },
499
+ // Zod v3 -> v4: Validation behavior differences
500
+ {
501
+ category: "validation-behavior",
502
+ migrations: ["zod-v3->v4"],
503
+ detect: (text, filePath) => {
504
+ const warnings = [];
505
+ if (/\.transform\s*\(/.test(text) && /\.refine\s*\(/.test(text)) {
506
+ warnings.push({
507
+ category: "validation-behavior",
508
+ message: ".transform() after .refine() behavior changed in Zod v4.",
509
+ detail: "In Zod v4, .transform() after .refine() may execute even if the refinement fails. Previously, transform was skipped on refinement failure.",
510
+ filePath,
511
+ severity: "warning",
512
+ migration: "zod-v3->v4"
513
+ });
514
+ }
515
+ return warnings;
516
+ }
517
+ },
518
+ // Zod -> Valibot: Error handling differences
519
+ {
520
+ category: "error-handling",
521
+ migrations: ["zod->valibot"],
522
+ detect: (text, filePath) => {
523
+ const warnings = [];
524
+ if (/\.parse\s*\(/.test(text) && /z\./.test(text)) {
525
+ warnings.push({
526
+ category: "error-handling",
527
+ message: "Zod .parse() throws ZodError; Valibot v.parse() throws ValiError.",
528
+ detail: "Error class and structure differ between Zod and Valibot. ZodError has .issues array; ValiError has .issues with different structure. Update all error handling code that inspects validation errors.",
529
+ filePath,
530
+ severity: "warning",
531
+ migration: "zod->valibot"
532
+ });
533
+ }
534
+ return warnings;
535
+ }
536
+ },
537
+ // io-ts -> Zod: Either monad vs throw/safeParse
538
+ {
539
+ category: "error-handling",
540
+ migrations: ["io-ts->zod"],
541
+ detect: (text, filePath) => {
542
+ const warnings = [];
543
+ if (/\bEither\b/.test(text) || /\b(fold|chain|map)\s*\(/.test(text)) {
544
+ warnings.push({
545
+ category: "error-handling",
546
+ message: "io-ts uses Either monad for errors; Zod uses throw/safeParse.",
547
+ detail: "io-ts returns Either<Errors, T> (Right for success, Left for failure). Zod .parse() throws, .safeParse() returns { success, data, error }. All fold/chain/map patterns over Either must be rewritten.",
548
+ filePath,
549
+ severity: "error",
550
+ migration: "io-ts->zod"
551
+ });
552
+ }
553
+ return warnings;
554
+ }
555
+ }
556
+ ];
557
+ var BehavioralWarningAnalyzer = class {
558
+ analyze(sourceFiles, from, to) {
559
+ const migration = `${from}->${to}`;
560
+ const warnings = [];
561
+ const applicableRules = BEHAVIORAL_RULES.filter((r) => r.migrations.includes(migration));
562
+ for (const sourceFile of sourceFiles) {
563
+ const filePath = sourceFile.getFilePath();
564
+ const text = sourceFile.getFullText();
565
+ const hasSourceLib = this.fileUsesLibrary(sourceFile, from);
566
+ if (!hasSourceLib) continue;
567
+ for (const rule of applicableRules) {
568
+ const ruleWarnings = rule.detect(text, filePath);
569
+ warnings.push(...ruleWarnings);
570
+ }
571
+ }
572
+ const summary = this.generateSummary(warnings, migration);
573
+ return { warnings, migrationPath: migration, summary };
574
+ }
575
+ fileUsesLibrary(sourceFile, library) {
576
+ for (const imp of sourceFile.getImportDeclarations()) {
577
+ const detected = detectSchemaLibrary(imp.getModuleSpecifierValue());
578
+ if (detected === library) return true;
579
+ if (library === "zod-v3" && detected === "zod") return true;
580
+ if (library === "zod" && detected === "zod") return true;
581
+ }
582
+ return false;
583
+ }
584
+ generateSummary(warnings, migration) {
585
+ if (warnings.length === 0) {
586
+ return `No behavioral differences detected for ${migration} migration.`;
587
+ }
588
+ const errorCount = warnings.filter((w) => w.severity === "error").length;
589
+ const warningCount = warnings.filter((w) => w.severity === "warning").length;
590
+ const infoCount = warnings.filter((w) => w.severity === "info").length;
591
+ const parts = [];
592
+ if (errorCount > 0) parts.push(`${errorCount} critical`);
593
+ if (warningCount > 0) parts.push(`${warningCount} warnings`);
594
+ if (infoCount > 0) parts.push(`${infoCount} info`);
595
+ return `Found ${warnings.length} behavioral difference(s) for ${migration}: ${parts.join(", ")}. Review before migrating.`;
596
+ }
597
+ };
598
+
599
+ // src/bundle-estimator.ts
600
+ var LIBRARY_SIZES = {
601
+ zod: { fullKb: 14, baseKb: 14, treeShakable: false },
602
+ "zod-v3": { fullKb: 14, baseKb: 14, treeShakable: false },
603
+ v4: { fullKb: 17.7, baseKb: 17.7, treeShakable: false },
604
+ "zod-v4": { fullKb: 17.7, baseKb: 17.7, treeShakable: false },
605
+ "zod-mini": { fullKb: 7.5, baseKb: 3.5, treeShakable: true },
606
+ yup: { fullKb: 13.6, baseKb: 13.6, treeShakable: false },
607
+ joi: { fullKb: 29.7, baseKb: 29.7, treeShakable: false },
608
+ "io-ts": { fullKb: 6.5, baseKb: 6.5, treeShakable: true },
609
+ valibot: { fullKb: 5.8, baseKb: 1.4, treeShakable: true }
610
+ };
611
+ var VALIDATOR_OVERHEAD = {
612
+ valibot: 0.05
613
+ };
614
+ var COMMON_VALIDATORS = /* @__PURE__ */ new Set([
615
+ "string",
616
+ "number",
617
+ "boolean",
618
+ "object",
619
+ "array",
620
+ "optional",
621
+ "nullable",
622
+ "enum",
623
+ "union",
624
+ "literal",
625
+ "date",
626
+ "email",
627
+ "url",
628
+ "uuid",
629
+ "min",
630
+ "max",
631
+ "regex",
632
+ "transform",
633
+ "refine",
634
+ "default",
635
+ "record",
636
+ "tuple",
637
+ "lazy",
638
+ "discriminatedUnion",
639
+ "intersection",
640
+ "partial",
641
+ "pick",
642
+ "omit",
643
+ "brand",
644
+ "pipe"
645
+ ]);
646
+ var BundleEstimator = class {
647
+ estimate(sourceFiles, from, to) {
648
+ const usedValidators = this.countUsedValidators(sourceFiles);
649
+ const fromInfo = this.getLibraryInfo(from, usedValidators);
650
+ const toInfo = this.getLibraryInfo(to, usedValidators);
651
+ const estimatedDelta = toInfo.estimatedUsedKb - fromInfo.estimatedUsedKb;
652
+ const deltaPercent = fromInfo.estimatedUsedKb > 0 ? Math.round(estimatedDelta / fromInfo.estimatedUsedKb * 100) : 0;
653
+ const caveats = this.generateCaveats(from, to, usedValidators);
654
+ const summary = this.generateSummary(fromInfo, toInfo, estimatedDelta, deltaPercent);
655
+ return {
656
+ from: fromInfo,
657
+ to: toInfo,
658
+ estimatedDelta,
659
+ deltaPercent,
660
+ summary,
661
+ caveats
662
+ };
663
+ }
664
+ countUsedValidators(sourceFiles) {
665
+ const usedSet = /* @__PURE__ */ new Set();
666
+ for (const file of sourceFiles) {
667
+ const text = file.getFullText();
668
+ for (const validator of COMMON_VALIDATORS) {
669
+ const pattern = new RegExp(`\\.${validator}\\s*[(<]`, "g");
670
+ if (pattern.test(text)) {
671
+ usedSet.add(validator);
672
+ }
673
+ }
674
+ }
675
+ return usedSet.size;
676
+ }
677
+ getLibraryInfo(library, usedValidators) {
678
+ const sizeKey = library === "zod-v3" ? "zod" : library;
679
+ const sizes = LIBRARY_SIZES[sizeKey] ?? { fullKb: 10, baseKb: 10, treeShakable: false };
680
+ let estimatedUsedKb;
681
+ if (sizes.treeShakable) {
682
+ const overhead = VALIDATOR_OVERHEAD[sizeKey] ?? 0.05;
683
+ estimatedUsedKb = Math.min(sizes.baseKb + usedValidators * overhead, sizes.fullKb);
684
+ } else {
685
+ estimatedUsedKb = sizes.fullKb;
686
+ }
687
+ return {
688
+ library: sizeKey,
689
+ minifiedGzipKb: sizes.fullKb,
690
+ treeShakable: sizes.treeShakable,
691
+ estimatedUsedKb: Math.round(estimatedUsedKb * 10) / 10
692
+ };
693
+ }
694
+ generateCaveats(from, to, _usedValidators) {
695
+ const caveats = [
696
+ "Sizes are estimates based on minified+gzipped bundle analysis.",
697
+ "Actual impact depends on bundler configuration, tree-shaking, and code splitting."
698
+ ];
699
+ if (to === "valibot") {
700
+ caveats.push(
701
+ "Valibot is fully tree-shakable \u2014 actual size depends on which validators you use."
702
+ );
703
+ caveats.push(
704
+ "Some developers report smaller-than-expected savings (6kB or less) in real projects."
705
+ );
706
+ }
707
+ if (from === "zod-v3" && to === "v4") {
708
+ caveats.push(
709
+ "Zod v4 is ~26% larger than v3 due to JIT compilation engine. Consider zod/mini for size-sensitive apps."
710
+ );
711
+ }
712
+ if (from === "joi") {
713
+ caveats.push(
714
+ "Joi is the largest schema library. Any migration will likely reduce bundle size."
715
+ );
716
+ }
717
+ return caveats;
718
+ }
719
+ generateSummary(from, to, delta, deltaPercent) {
720
+ const direction = delta > 0 ? "increase" : delta < 0 ? "decrease" : "no change";
721
+ const absDelta = Math.abs(Math.round(delta * 10) / 10);
722
+ return `Estimated bundle ${direction}: ${from.library} (${from.estimatedUsedKb}kB) \u2192 ${to.library} (${to.estimatedUsedKb}kB) = ${delta > 0 ? "+" : delta < 0 ? "-" : ""}${absDelta}kB (${deltaPercent > 0 ? "+" : ""}${deltaPercent}%)`;
723
+ }
724
+ };
725
+
267
726
  // src/chain.ts
268
727
  import { Project as Project2 } from "ts-morph";
269
728
  var MigrationChain = class {
@@ -330,12 +789,12 @@ var MigrationChain = class {
330
789
  };
331
790
 
332
791
  // src/compatibility.ts
333
- import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
334
- import { join as join2 } from "path";
792
+ import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
793
+ import { join as join3 } from "path";
335
794
 
336
795
  // src/ecosystem.ts
337
- import { existsSync, readFileSync } from "fs";
338
- import { join } from "path";
796
+ import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
797
+ import { join as join2 } from "path";
339
798
  var ECOSYSTEM_RULES = [
340
799
  // ORM integrations
341
800
  {
@@ -345,7 +804,8 @@ var ECOSYSTEM_RULES = [
345
804
  check: () => ({
346
805
  issue: "drizzle-zod may not support Zod v4. Check for a compatible version before upgrading.",
347
806
  suggestion: "Upgrade drizzle-zod to the latest version that supports Zod v4, or use --legacy-peer-deps.",
348
- severity: "warning"
807
+ severity: "warning",
808
+ upgradeCommand: "npm install drizzle-zod@latest"
349
809
  })
350
810
  },
351
811
  {
@@ -355,7 +815,8 @@ var ECOSYSTEM_RULES = [
355
815
  check: () => ({
356
816
  issue: "zod-prisma generates Zod v3 schemas. Generated files will need regeneration after upgrading to Zod v4.",
357
817
  suggestion: "Upgrade zod-prisma to a v4-compatible version and regenerate schemas.",
358
- severity: "warning"
818
+ severity: "warning",
819
+ upgradeCommand: "npm install zod-prisma@latest"
359
820
  })
360
821
  },
361
822
  {
@@ -365,7 +826,8 @@ var ECOSYSTEM_RULES = [
365
826
  check: () => ({
366
827
  issue: "zod-prisma-types generates Zod v3 schemas. Generated files will need regeneration.",
367
828
  suggestion: "Check for a Zod v4-compatible version of zod-prisma-types.",
368
- severity: "warning"
829
+ severity: "warning",
830
+ upgradeCommand: "npm install zod-prisma-types@latest"
369
831
  })
370
832
  },
371
833
  // API framework integrations
@@ -380,7 +842,8 @@ var ECOSYSTEM_RULES = [
380
842
  return {
381
843
  issue: `tRPC v${major} expects Zod v3 types. A v3 ZodType is not assignable to a v4 ZodType.`,
382
844
  suggestion: "Upgrade to tRPC v11+ which supports Zod v4 via Standard Schema.",
383
- severity: "error"
845
+ severity: "error",
846
+ upgradeCommand: "npm install @trpc/server@latest"
384
847
  };
385
848
  }
386
849
  return {
@@ -397,7 +860,8 @@ var ECOSYSTEM_RULES = [
397
860
  check: () => ({
398
861
  issue: "trpc-ui breaks entirely with Zod v4 schemas.",
399
862
  suggestion: "Check for a Zod v4-compatible version of trpc-ui before upgrading.",
400
- severity: "error"
863
+ severity: "error",
864
+ upgradeCommand: "npm install trpc-ui@latest"
401
865
  })
402
866
  },
403
867
  // Validation utilities
@@ -412,7 +876,8 @@ var ECOSYSTEM_RULES = [
412
876
  return {
413
877
  issue: `zod-validation-error v${major} is not compatible with Zod v4.`,
414
878
  suggestion: "Upgrade zod-validation-error to v5.0.0+ for Zod v4 support.",
415
- severity: "error"
879
+ severity: "error",
880
+ upgradeCommand: "npm install zod-validation-error@^5.0.0"
416
881
  };
417
882
  }
418
883
  return null;
@@ -428,13 +893,15 @@ var ECOSYSTEM_RULES = [
428
893
  return {
429
894
  issue: "@hookform/resolvers zodResolver may need updating for Zod v4.",
430
895
  suggestion: "Upgrade @hookform/resolvers to the latest version with Zod v4 support.",
431
- severity: "warning"
896
+ severity: "warning",
897
+ upgradeCommand: "npm install @hookform/resolvers@latest"
432
898
  };
433
899
  }
434
900
  return {
435
901
  issue: "@hookform/resolvers will need its resolver import updated for the new schema library.",
436
902
  suggestion: "Switch from the old resolver (e.g., yupResolver) to zodResolver from @hookform/resolvers/zod.",
437
- severity: "warning"
903
+ severity: "warning",
904
+ upgradeCommand: "npm install @hookform/resolvers@latest"
438
905
  };
439
906
  }
440
907
  },
@@ -466,7 +933,8 @@ var ECOSYSTEM_RULES = [
466
933
  check: () => ({
467
934
  issue: "zod-openapi may not support Zod v4 yet.",
468
935
  suggestion: "Check for a Zod v4-compatible version of zod-openapi.",
469
- severity: "warning"
936
+ severity: "warning",
937
+ upgradeCommand: "npm install zod-openapi@latest"
470
938
  })
471
939
  },
472
940
  {
@@ -476,7 +944,60 @@ var ECOSYSTEM_RULES = [
476
944
  check: () => ({
477
945
  issue: "@asteasolutions/zod-to-openapi may not support Zod v4 yet.",
478
946
  suggestion: "Check for a Zod v4-compatible version of @asteasolutions/zod-to-openapi.",
479
- severity: "warning"
947
+ severity: "warning",
948
+ upgradeCommand: "npm install @asteasolutions/zod-to-openapi@latest"
949
+ })
950
+ },
951
+ // AI/MCP integrations
952
+ {
953
+ package: "@modelcontextprotocol/sdk",
954
+ category: "api",
955
+ migrations: ["zod-v3->v4"],
956
+ check: () => ({
957
+ issue: "MCP SDK may have Zod v4 compatibility issues. MCP servers typically expect Zod v3 schemas.",
958
+ suggestion: "Check MCP SDK release notes for Zod v4 support before upgrading. Consider staying on Zod v3 for MCP servers.",
959
+ severity: "warning",
960
+ upgradeCommand: "npm install @modelcontextprotocol/sdk@latest"
961
+ })
962
+ },
963
+ {
964
+ package: "@openai/agents",
965
+ category: "api",
966
+ migrations: ["zod-v3->v4"],
967
+ check: () => ({
968
+ issue: "OpenAI Agents SDK recommends pinning to zod@3.25.67 due to TS2589 errors with newer versions.",
969
+ suggestion: "Pin zod to 3.25.67 for OpenAI Agents SDK compatibility, or wait for an SDK update with Zod v4 support.",
970
+ severity: "error"
971
+ })
972
+ },
973
+ // Additional validation utilities
974
+ {
975
+ package: "zod-to-json-schema",
976
+ category: "validation-util",
977
+ migrations: ["zod-v3->v4"],
978
+ check: (version) => {
979
+ const majorMatch = version.match(/(\d+)/);
980
+ const major = majorMatch?.[1] ? Number.parseInt(majorMatch[1], 10) : 0;
981
+ if (major < 4) {
982
+ return {
983
+ issue: "zod-to-json-schema v3 may not fully support Zod v4 schemas.",
984
+ suggestion: "Upgrade to zod-to-json-schema v4+ for full Zod v4 support.",
985
+ severity: "warning",
986
+ upgradeCommand: "npm install zod-to-json-schema@latest"
987
+ };
988
+ }
989
+ return null;
990
+ }
991
+ },
992
+ {
993
+ package: "react-hook-form",
994
+ category: "form",
995
+ migrations: ["zod-v3->v4"],
996
+ check: () => ({
997
+ issue: "React Hook Form with zodResolver may throw uncaught ZodError instead of populating formState.errors with Zod v4.",
998
+ suggestion: "Upgrade @hookform/resolvers to the latest version and test form validation thoroughly.",
999
+ severity: "warning",
1000
+ upgradeCommand: "npm install @hookform/resolvers@latest react-hook-form@latest"
480
1001
  })
481
1002
  }
482
1003
  ];
@@ -486,13 +1007,13 @@ var EcosystemAnalyzer = class {
486
1007
  const dependencies = [];
487
1008
  const warnings = [];
488
1009
  const blockers = [];
489
- const pkgPath = join(projectPath, "package.json");
490
- if (!existsSync(pkgPath)) {
1010
+ const pkgPath = join2(projectPath, "package.json");
1011
+ if (!existsSync2(pkgPath)) {
491
1012
  return { dependencies, warnings, blockers };
492
1013
  }
493
1014
  let allDeps = {};
494
1015
  try {
495
- const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
1016
+ const pkg = JSON.parse(readFileSync2(pkgPath, "utf-8"));
496
1017
  allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
497
1018
  } catch {
498
1019
  return { dependencies, warnings, blockers };
@@ -510,7 +1031,8 @@ var EcosystemAnalyzer = class {
510
1031
  issue: result.issue,
511
1032
  suggestion: result.suggestion,
512
1033
  severity: result.severity,
513
- category: rule.category
1034
+ category: rule.category,
1035
+ ...result.upgradeCommand ? { upgradeCommand: result.upgradeCommand } : {}
514
1036
  };
515
1037
  dependencies.push(issue);
516
1038
  if (result.severity === "error") {
@@ -521,6 +1043,20 @@ var EcosystemAnalyzer = class {
521
1043
  }
522
1044
  return { dependencies, warnings, blockers };
523
1045
  }
1046
+ /**
1047
+ * Returns a list of npm install commands needed to resolve ecosystem issues.
1048
+ */
1049
+ getUpgradeCommands(report) {
1050
+ const commands = [];
1051
+ const seen = /* @__PURE__ */ new Set();
1052
+ for (const dep of report.dependencies) {
1053
+ if (dep.upgradeCommand && !seen.has(dep.upgradeCommand)) {
1054
+ seen.add(dep.upgradeCommand);
1055
+ commands.push(dep.upgradeCommand);
1056
+ }
1057
+ }
1058
+ return commands;
1059
+ }
524
1060
  };
525
1061
 
526
1062
  // src/compatibility.ts
@@ -598,10 +1134,10 @@ var CompatibilityAnalyzer = class {
598
1134
  ecosystemAnalyzer = new EcosystemAnalyzer();
599
1135
  detectVersions(projectPath) {
600
1136
  const versions = [];
601
- const pkgPath = join2(projectPath, "package.json");
602
- if (!existsSync2(pkgPath)) return versions;
1137
+ const pkgPath = join3(projectPath, "package.json");
1138
+ if (!existsSync3(pkgPath)) return versions;
603
1139
  try {
604
- const pkg = JSON.parse(readFileSync2(pkgPath, "utf-8"));
1140
+ const pkg = JSON.parse(readFileSync3(pkgPath, "utf-8"));
605
1141
  const knownLibs = ["zod", "yup", "joi", "io-ts", "valibot"];
606
1142
  const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
607
1143
  for (const lib of knownLibs) {
@@ -648,6 +1184,120 @@ var CompatibilityAnalyzer = class {
648
1184
  }
649
1185
  };
650
1186
 
1187
+ // src/complexity-estimator.ts
1188
+ var SCHEMA_FACTORY_PATTERN = /z\.(object|string|number|boolean|array|enum|union|discriminatedUnion|intersection|lazy|tuple|record|literal|nativeEnum|any|unknown|void|null|undefined|never|date|bigint|symbol|function|promise|map|set|custom|preprocess|pipeline|brand|coerce)\s*\(/g;
1189
+ var ADVANCED_PATTERNS = [
1190
+ [/z\.discriminatedUnion\s*\(/g, "discriminatedUnion"],
1191
+ [/z\.intersection\s*\(/g, "intersection"],
1192
+ [/z\.lazy\s*\(/g, "recursive"],
1193
+ [/\.brand\s*[<(]/g, "branded"],
1194
+ [/\.superRefine\s*\(/g, "superRefine"],
1195
+ [/\.transform\s*\(/g, "transform"],
1196
+ [/\.pipe\s*\(/g, "pipe"],
1197
+ [/\.refine\s*\(/g, "refine"]
1198
+ ];
1199
+ function countMatches(text, pattern) {
1200
+ pattern.lastIndex = 0;
1201
+ let count = 0;
1202
+ while (pattern.exec(text)) count++;
1203
+ return count;
1204
+ }
1205
+ function getMaxChainDepth(text) {
1206
+ let maxDepth = 0;
1207
+ const lines = text.split("\n");
1208
+ for (const line of lines) {
1209
+ const dotCalls = line.match(/\.\w+\s*\(/g);
1210
+ if (dotCalls && dotCalls.length > maxDepth) {
1211
+ maxDepth = dotCalls.length;
1212
+ }
1213
+ }
1214
+ return maxDepth;
1215
+ }
1216
+ var ComplexityEstimator = class {
1217
+ estimate(files) {
1218
+ const fileResults = [];
1219
+ const warnings = [];
1220
+ const riskAreas = [];
1221
+ let totalSchemas = 0;
1222
+ let advancedPatternCount = 0;
1223
+ let hasDeepDiscriminatedUnions = false;
1224
+ for (const file of files) {
1225
+ const text = file.getFullText();
1226
+ const filePath = file.getFilePath();
1227
+ const lineCount = file.getEndLineNumber();
1228
+ const schemaCount = countMatches(text, new RegExp(SCHEMA_FACTORY_PATTERN.source, "g"));
1229
+ totalSchemas += schemaCount;
1230
+ const advancedPatterns = [];
1231
+ for (const [pattern, name] of ADVANCED_PATTERNS) {
1232
+ const count = countMatches(text, new RegExp(pattern.source, "g"));
1233
+ if (count > 0) {
1234
+ advancedPatterns.push(name);
1235
+ advancedPatternCount += count;
1236
+ }
1237
+ }
1238
+ const chainDepth = getMaxChainDepth(text);
1239
+ fileResults.push({ filePath, schemaCount, advancedPatterns, chainDepth, lineCount });
1240
+ if (lineCount > 500) {
1241
+ warnings.push({
1242
+ file: filePath,
1243
+ message: `Large file (${lineCount} lines) may be difficult to migrate in one pass`,
1244
+ severity: "warning"
1245
+ });
1246
+ }
1247
+ if (schemaCount > 50) {
1248
+ warnings.push({
1249
+ file: filePath,
1250
+ message: `High schema density (${schemaCount} schemas) \u2014 consider splitting before migration`,
1251
+ severity: "warning"
1252
+ });
1253
+ }
1254
+ if (chainDepth > 20) {
1255
+ warnings.push({
1256
+ file: filePath,
1257
+ message: `Long method chain (${chainDepth} calls) \u2014 higher transformation risk`,
1258
+ severity: "warning"
1259
+ });
1260
+ }
1261
+ const duCount = countMatches(text, /z\.discriminatedUnion\s*\(/g);
1262
+ if (duCount > 10) {
1263
+ hasDeepDiscriminatedUnions = true;
1264
+ warnings.push({
1265
+ file: filePath,
1266
+ message: `${duCount} discriminated unions \u2014 TypeScript TS2589 performance risk in Zod v4`,
1267
+ severity: "error"
1268
+ });
1269
+ }
1270
+ if (advancedPatterns.includes("recursive")) {
1271
+ riskAreas.push(`Recursive schemas in ${filePath}`);
1272
+ }
1273
+ if (advancedPatterns.includes("branded")) {
1274
+ riskAreas.push(`Branded types in ${filePath}`);
1275
+ }
1276
+ }
1277
+ const effort = this.calculateEffort(
1278
+ totalSchemas,
1279
+ advancedPatternCount,
1280
+ hasDeepDiscriminatedUnions
1281
+ );
1282
+ return {
1283
+ effort,
1284
+ totalSchemas,
1285
+ totalFiles: files.length,
1286
+ advancedPatternCount,
1287
+ files: fileResults,
1288
+ warnings,
1289
+ riskAreas
1290
+ };
1291
+ }
1292
+ calculateEffort(totalSchemas, advancedCount, hasDeepDU) {
1293
+ if (totalSchemas >= 500 && hasDeepDU) return "extreme";
1294
+ if (totalSchemas >= 200 || advancedCount >= 20) return "high";
1295
+ if (totalSchemas >= 50 || advancedCount >= 5) return "moderate";
1296
+ if (totalSchemas >= 10) return "low";
1297
+ return "trivial";
1298
+ }
1299
+ };
1300
+
651
1301
  // src/config.ts
652
1302
  import { cosmiconfig } from "cosmiconfig";
653
1303
  function validateConfig(config) {
@@ -709,6 +1359,8 @@ async function loadConfig(configPath) {
709
1359
  }
710
1360
 
711
1361
  // src/dependency-graph.ts
1362
+ import { existsSync as existsSync4, readdirSync, readFileSync as readFileSync4 } from "fs";
1363
+ import { join as join4, resolve } from "path";
712
1364
  var SchemaDependencyResolver = class {
713
1365
  resolve(project, filePaths) {
714
1366
  const fileSet = new Set(filePaths);
@@ -794,10 +1446,214 @@ var SchemaDependencyResolver = class {
794
1446
  return parts.slice(-2).join("/");
795
1447
  }
796
1448
  };
1449
+ var SCHEMA_PACKAGES = /* @__PURE__ */ new Set(["zod", "yup", "joi", "io-ts", "valibot", "@effect/schema"]);
1450
+ function computeParallelBatches(packages, suggestedOrder) {
1451
+ const nameSet = new Set(packages.map((p) => p.name));
1452
+ const depMap = /* @__PURE__ */ new Map();
1453
+ for (const pkg of packages) {
1454
+ depMap.set(pkg.name, new Set(pkg.dependencies.filter((d) => nameSet.has(d))));
1455
+ }
1456
+ const depths = /* @__PURE__ */ new Map();
1457
+ const getDepth = (name, visited) => {
1458
+ const cached = depths.get(name);
1459
+ if (cached !== void 0) return cached;
1460
+ if (visited.has(name)) return 0;
1461
+ visited.add(name);
1462
+ const deps = depMap.get(name) ?? /* @__PURE__ */ new Set();
1463
+ let maxDepth = 0;
1464
+ for (const dep of deps) {
1465
+ maxDepth = Math.max(maxDepth, getDepth(dep, visited) + 1);
1466
+ }
1467
+ depths.set(name, maxDepth);
1468
+ return maxDepth;
1469
+ };
1470
+ for (const name of suggestedOrder) {
1471
+ getDepth(name, /* @__PURE__ */ new Set());
1472
+ }
1473
+ const batchMap = /* @__PURE__ */ new Map();
1474
+ for (const name of suggestedOrder) {
1475
+ const depth = depths.get(name) ?? 0;
1476
+ const batch = batchMap.get(depth) ?? [];
1477
+ batch.push(name);
1478
+ batchMap.set(depth, batch);
1479
+ }
1480
+ const batches = [];
1481
+ const sortedDepths = [...batchMap.keys()].sort((a, b) => a - b);
1482
+ for (const depth of sortedDepths) {
1483
+ const pkgs = batchMap.get(depth);
1484
+ if (pkgs) batches.push({ index: batches.length, packages: pkgs });
1485
+ }
1486
+ return batches;
1487
+ }
1488
+ var MonorepoResolver = class {
1489
+ detect(projectPath) {
1490
+ const pkgPath = join4(projectPath, "package.json");
1491
+ if (existsSync4(pkgPath)) {
1492
+ try {
1493
+ const pkg = JSON.parse(readFileSync4(pkgPath, "utf-8"));
1494
+ if (pkg.workspaces) return true;
1495
+ } catch {
1496
+ }
1497
+ }
1498
+ if (existsSync4(join4(projectPath, "pnpm-workspace.yaml"))) return true;
1499
+ return false;
1500
+ }
1501
+ /**
1502
+ * Detect which workspace manager is being used.
1503
+ */
1504
+ detectManager(projectPath) {
1505
+ if (existsSync4(join4(projectPath, "pnpm-workspace.yaml"))) return "pnpm";
1506
+ const pkgPath = join4(projectPath, "package.json");
1507
+ if (existsSync4(pkgPath)) {
1508
+ try {
1509
+ const pkg = JSON.parse(readFileSync4(pkgPath, "utf-8"));
1510
+ if (pkg.packageManager?.startsWith("yarn")) return "yarn";
1511
+ if (pkg.packageManager?.startsWith("pnpm")) return "pnpm";
1512
+ } catch {
1513
+ }
1514
+ }
1515
+ if (existsSync4(join4(projectPath, "pnpm-lock.yaml"))) return "pnpm";
1516
+ if (existsSync4(join4(projectPath, "yarn.lock"))) return "yarn";
1517
+ return "npm";
1518
+ }
1519
+ analyze(projectPath) {
1520
+ const pkgPath = join4(projectPath, "package.json");
1521
+ if (!existsSync4(pkgPath)) {
1522
+ return { isMonorepo: false, packages: [], suggestedOrder: [] };
1523
+ }
1524
+ let workspaceGlobs;
1525
+ try {
1526
+ workspaceGlobs = this.resolveWorkspaceGlobs(projectPath);
1527
+ if (workspaceGlobs.length === 0) {
1528
+ return { isMonorepo: false, packages: [], suggestedOrder: [] };
1529
+ }
1530
+ } catch {
1531
+ return { isMonorepo: false, packages: [], suggestedOrder: [] };
1532
+ }
1533
+ const packages = [];
1534
+ const resolvedDirs = this.resolveWorkspaceDirs(projectPath, workspaceGlobs);
1535
+ for (const dir of resolvedDirs) {
1536
+ const wsPkgPath = join4(dir, "package.json");
1537
+ if (!existsSync4(wsPkgPath)) continue;
1538
+ try {
1539
+ const wsPkg = JSON.parse(readFileSync4(wsPkgPath, "utf-8"));
1540
+ if (!wsPkg.name) continue;
1541
+ const allDeps = { ...wsPkg.dependencies, ...wsPkg.devDependencies };
1542
+ const depNames = Object.keys(allDeps);
1543
+ const schemaLibrary = depNames.find((d) => SCHEMA_PACKAGES.has(d));
1544
+ packages.push({
1545
+ name: wsPkg.name,
1546
+ path: dir,
1547
+ schemaLibrary,
1548
+ dependencies: depNames
1549
+ });
1550
+ } catch {
1551
+ }
1552
+ }
1553
+ const suggestedOrder = this.suggestOrder(packages);
1554
+ return { isMonorepo: true, packages, suggestedOrder };
1555
+ }
1556
+ suggestOrder(packages) {
1557
+ const nameSet = new Set(packages.map((p) => p.name));
1558
+ const depMap = /* @__PURE__ */ new Map();
1559
+ for (const pkg of packages) {
1560
+ const internalDeps = pkg.dependencies.filter((d) => nameSet.has(d));
1561
+ depMap.set(pkg.name, internalDeps);
1562
+ }
1563
+ const visited = /* @__PURE__ */ new Set();
1564
+ const sorted = [];
1565
+ const visit = (name) => {
1566
+ if (visited.has(name)) return;
1567
+ visited.add(name);
1568
+ for (const dep of depMap.get(name) ?? []) {
1569
+ visit(dep);
1570
+ }
1571
+ sorted.push(name);
1572
+ };
1573
+ for (const pkg of packages) {
1574
+ visit(pkg.name);
1575
+ }
1576
+ return sorted;
1577
+ }
1578
+ /**
1579
+ * Resolve workspace glob patterns from any supported format.
1580
+ * Supports: npm/yarn workspaces (package.json), pnpm-workspace.yaml
1581
+ */
1582
+ resolveWorkspaceGlobs(projectPath) {
1583
+ const pnpmPath = join4(projectPath, "pnpm-workspace.yaml");
1584
+ if (existsSync4(pnpmPath)) {
1585
+ return this.parsePnpmWorkspace(pnpmPath);
1586
+ }
1587
+ const pkgPath = join4(projectPath, "package.json");
1588
+ if (existsSync4(pkgPath)) {
1589
+ try {
1590
+ const pkg = JSON.parse(readFileSync4(pkgPath, "utf-8"));
1591
+ if (pkg.workspaces) {
1592
+ return Array.isArray(pkg.workspaces) ? pkg.workspaces : pkg.workspaces.packages;
1593
+ }
1594
+ } catch {
1595
+ }
1596
+ }
1597
+ return [];
1598
+ }
1599
+ /**
1600
+ * Parse pnpm-workspace.yaml to extract workspace package globs.
1601
+ * Simple YAML parsing for the common format:
1602
+ * ```
1603
+ * packages:
1604
+ * - 'packages/*'
1605
+ * - 'apps/*'
1606
+ * ```
1607
+ */
1608
+ parsePnpmWorkspace(filePath) {
1609
+ const content = readFileSync4(filePath, "utf-8");
1610
+ const globs = [];
1611
+ let inPackages = false;
1612
+ for (const line of content.split("\n")) {
1613
+ const trimmed = line.trim();
1614
+ if (trimmed === "packages:") {
1615
+ inPackages = true;
1616
+ continue;
1617
+ }
1618
+ if (inPackages && /^\w/.test(trimmed) && !trimmed.startsWith("-")) {
1619
+ break;
1620
+ }
1621
+ if (inPackages && trimmed.startsWith("-")) {
1622
+ const pattern = trimmed.replace(/^-\s*/, "").replace(/^['"]|['"]$/g, "");
1623
+ if (pattern) {
1624
+ globs.push(pattern);
1625
+ }
1626
+ }
1627
+ }
1628
+ return globs;
1629
+ }
1630
+ resolveWorkspaceDirs(projectPath, globs) {
1631
+ const dirs = [];
1632
+ for (const glob of globs) {
1633
+ const clean = glob.replace(/\/?\*$/, "");
1634
+ const base = resolve(projectPath, clean);
1635
+ if (!existsSync4(base)) continue;
1636
+ if (glob.endsWith("*")) {
1637
+ try {
1638
+ const entries = readdirSync(base, { withFileTypes: true });
1639
+ for (const entry of entries) {
1640
+ if (entry.isDirectory()) {
1641
+ dirs.push(join4(base, entry.name));
1642
+ }
1643
+ }
1644
+ } catch {
1645
+ }
1646
+ } else {
1647
+ dirs.push(base);
1648
+ }
1649
+ }
1650
+ return dirs;
1651
+ }
1652
+ };
797
1653
 
798
1654
  // src/detailed-analyzer.ts
799
- import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
800
- import { join as join3 } from "path";
1655
+ import { existsSync as existsSync5, readFileSync as readFileSync5 } from "fs";
1656
+ import { join as join5 } from "path";
801
1657
  var COMPLEXITY_CHAIN_WEIGHT = 2;
802
1658
  var COMPLEXITY_DEPTH_WEIGHT = 3;
803
1659
  var COMPLEXITY_VALIDATION_WEIGHT = 1;
@@ -862,10 +1718,10 @@ var DetailedAnalyzer = class {
862
1718
  }
863
1719
  detectLibraryVersions(projectPath) {
864
1720
  const versions = [];
865
- const pkgPath = join3(projectPath, "package.json");
866
- if (!existsSync3(pkgPath)) return versions;
1721
+ const pkgPath = join5(projectPath, "package.json");
1722
+ if (!existsSync5(pkgPath)) return versions;
867
1723
  try {
868
- const pkg = JSON.parse(readFileSync3(pkgPath, "utf-8"));
1724
+ const pkg = JSON.parse(readFileSync5(pkgPath, "utf-8"));
869
1725
  const knownLibs = ["zod", "yup", "joi", "io-ts", "valibot"];
870
1726
  const allDeps = {
871
1727
  ...pkg.dependencies,
@@ -1038,9 +1894,94 @@ var DetailedAnalyzer = class {
1038
1894
  }
1039
1895
  };
1040
1896
 
1897
+ // src/form-resolver-migrator.ts
1898
+ var RESOLVER_MAPPINGS = {
1899
+ "yup->zod": [
1900
+ {
1901
+ fromImport: "@hookform/resolvers/yup",
1902
+ toImport: "@hookform/resolvers/zod",
1903
+ fromResolver: "yupResolver",
1904
+ toResolver: "zodResolver"
1905
+ }
1906
+ ],
1907
+ "joi->zod": [
1908
+ {
1909
+ fromImport: "@hookform/resolvers/joi",
1910
+ toImport: "@hookform/resolvers/zod",
1911
+ fromResolver: "joiResolver",
1912
+ toResolver: "zodResolver"
1913
+ }
1914
+ ],
1915
+ "zod->valibot": [
1916
+ {
1917
+ fromImport: "@hookform/resolvers/zod",
1918
+ toImport: "@hookform/resolvers/valibot",
1919
+ fromResolver: "zodResolver",
1920
+ toResolver: "valibotResolver"
1921
+ }
1922
+ ]
1923
+ };
1924
+ var TODO_PATTERNS = [
1925
+ {
1926
+ pattern: /from\s+['"]formik['"]/,
1927
+ comment: "/* TODO(schemashift): Formik is unmaintained. Consider migrating to React Hook Form with zodResolver */"
1928
+ },
1929
+ {
1930
+ pattern: /from\s+['"]@mantine\/form['"]/,
1931
+ comment: "/* TODO(schemashift): Update @mantine/form to use Zod schema adapter */"
1932
+ }
1933
+ ];
1934
+ var FormResolverMigrator = class {
1935
+ migrate(sourceFile, from, to) {
1936
+ const migration = `${from}->${to}`;
1937
+ let code = sourceFile.getFullText();
1938
+ const changes = [];
1939
+ const warnings = [];
1940
+ const mappings = RESOLVER_MAPPINGS[migration];
1941
+ if (mappings) {
1942
+ for (const mapping of mappings) {
1943
+ if (code.includes(mapping.fromImport)) {
1944
+ code = code.replaceAll(mapping.fromImport, mapping.toImport);
1945
+ code = code.replaceAll(mapping.fromResolver, mapping.toResolver);
1946
+ changes.push(
1947
+ `Replaced ${mapping.fromResolver} import from '${mapping.fromImport}' with ${mapping.toResolver} from '${mapping.toImport}'`
1948
+ );
1949
+ }
1950
+ }
1951
+ }
1952
+ const lines = code.split("\n");
1953
+ const insertions = [];
1954
+ for (let i = 0; i < lines.length; i++) {
1955
+ const line = lines[i] ?? "";
1956
+ for (const { pattern, comment } of TODO_PATTERNS) {
1957
+ if (pattern.test(line) && !code.includes(comment)) {
1958
+ insertions.push({ index: i, comment });
1959
+ warnings.push(comment.replace(/\/\*\s*|\s*\*\//g, "").trim());
1960
+ }
1961
+ }
1962
+ }
1963
+ for (let i = insertions.length - 1; i >= 0; i--) {
1964
+ const insertion = insertions[i];
1965
+ if (!insertion) continue;
1966
+ lines.splice(insertion.index, 0, insertion.comment);
1967
+ changes.push(`Added TODO comment for ${lines[insertion.index + 1]?.trim()}`);
1968
+ }
1969
+ if (insertions.length > 0) {
1970
+ code = lines.join("\n");
1971
+ }
1972
+ return {
1973
+ success: true,
1974
+ transformedCode: code,
1975
+ changes,
1976
+ warnings
1977
+ };
1978
+ }
1979
+ };
1980
+
1041
1981
  // src/governance.ts
1042
1982
  var GovernanceEngine = class {
1043
1983
  rules = /* @__PURE__ */ new Map();
1984
+ customRuleFunctions = /* @__PURE__ */ new Map();
1044
1985
  configure(rules) {
1045
1986
  this.rules.clear();
1046
1987
  for (const [name, config] of Object.entries(rules)) {
@@ -1049,6 +1990,13 @@ var GovernanceEngine = class {
1049
1990
  }
1050
1991
  }
1051
1992
  }
1993
+ /**
1994
+ * Register a custom governance rule function.
1995
+ * Custom rules are executed per-file alongside built-in rules.
1996
+ */
1997
+ registerRule(name, fn) {
1998
+ this.customRuleFunctions.set(name, fn);
1999
+ }
1052
2000
  analyze(project) {
1053
2001
  const violations = [];
1054
2002
  let schemasChecked = 0;
@@ -1124,6 +2072,104 @@ var GovernanceEngine = class {
1124
2072
  });
1125
2073
  }
1126
2074
  }
2075
+ if (this.rules.has("require-safeParse")) {
2076
+ if (text.includes(".parse(") && !text.includes(".safeParse(")) {
2077
+ violations.push({
2078
+ rule: "require-safeParse",
2079
+ message: `Schema "${schemaName}" uses .parse() \u2014 prefer .safeParse() for safer error handling`,
2080
+ filePath,
2081
+ lineNumber,
2082
+ schemaName,
2083
+ severity: "warning",
2084
+ fixable: true
2085
+ });
2086
+ }
2087
+ }
2088
+ if (this.rules.has("require-description")) {
2089
+ if (!text.includes(".describe(")) {
2090
+ violations.push({
2091
+ rule: "require-description",
2092
+ message: `Schema "${schemaName}" missing .describe() \u2014 add a description for documentation`,
2093
+ filePath,
2094
+ lineNumber,
2095
+ schemaName,
2096
+ severity: "warning",
2097
+ fixable: true
2098
+ });
2099
+ }
2100
+ }
2101
+ if (this.rules.has("no-coerce-in-api")) {
2102
+ if (/\.coerce\./.test(text)) {
2103
+ violations.push({
2104
+ rule: "no-coerce-in-api",
2105
+ message: `Schema "${schemaName}" uses z.coerce.* \u2014 coercion in API validation is a security risk`,
2106
+ filePath,
2107
+ lineNumber,
2108
+ schemaName,
2109
+ severity: "error",
2110
+ fixable: false
2111
+ });
2112
+ }
2113
+ }
2114
+ if (this.rules.has("require-max-length")) {
2115
+ if (text.includes(".string()") && !text.includes(".max(") && !text.includes(".length(")) {
2116
+ violations.push({
2117
+ rule: "require-max-length",
2118
+ message: `Schema "${schemaName}" has string without max length \u2014 required for DoS prevention`,
2119
+ filePath,
2120
+ lineNumber,
2121
+ schemaName,
2122
+ severity: "error",
2123
+ fixable: true
2124
+ });
2125
+ }
2126
+ }
2127
+ if (this.rules.has("max-nesting-depth")) {
2128
+ const config = this.rules.get("max-nesting-depth") ?? {};
2129
+ const maxDepth = config.threshold ?? 5;
2130
+ const depth = this.measureNestingDepth(text);
2131
+ if (depth > maxDepth) {
2132
+ violations.push({
2133
+ rule: "max-nesting-depth",
2134
+ message: `Schema "${schemaName}" nesting depth (${depth}) exceeds limit (${maxDepth})`,
2135
+ filePath,
2136
+ lineNumber,
2137
+ schemaName,
2138
+ severity: "warning",
2139
+ fixable: false
2140
+ });
2141
+ }
2142
+ }
2143
+ }
2144
+ }
2145
+ for (const sourceFile of project.getSourceFiles()) {
2146
+ const library = this.detectFileLibrary(sourceFile);
2147
+ if (library === "unknown") continue;
2148
+ const filePath = sourceFile.getFilePath();
2149
+ const text = sourceFile.getFullText();
2150
+ if (this.rules.has("no-dynamic-schemas")) {
2151
+ const dynamicPatterns = this.detectDynamicSchemas(text, library);
2152
+ for (const lineNumber of dynamicPatterns) {
2153
+ violations.push({
2154
+ rule: "no-dynamic-schemas",
2155
+ message: "Schema created inside function body \u2014 move to module level for performance",
2156
+ filePath,
2157
+ lineNumber,
2158
+ schemaName: "(dynamic)",
2159
+ severity: "warning",
2160
+ fixable: false
2161
+ });
2162
+ }
2163
+ }
2164
+ }
2165
+ for (const [ruleName, ruleFn] of this.customRuleFunctions) {
2166
+ const config = this.rules.get(ruleName);
2167
+ if (!config) continue;
2168
+ for (const sourceFile of project.getSourceFiles()) {
2169
+ const library = this.detectFileLibrary(sourceFile);
2170
+ if (library === "unknown") continue;
2171
+ const ruleViolations = ruleFn(sourceFile, config);
2172
+ violations.push(...ruleViolations);
1127
2173
  }
1128
2174
  }
1129
2175
  return {
@@ -1140,6 +2186,57 @@ var GovernanceEngine = class {
1140
2186
  }
1141
2187
  return "unknown";
1142
2188
  }
2189
+ measureNestingDepth(text) {
2190
+ let maxDepth = 0;
2191
+ let current = 0;
2192
+ for (const char of text) {
2193
+ if (char === "(") {
2194
+ current++;
2195
+ if (current > maxDepth) maxDepth = current;
2196
+ } else if (char === ")") {
2197
+ current--;
2198
+ }
2199
+ }
2200
+ return maxDepth;
2201
+ }
2202
+ detectDynamicSchemas(text, library) {
2203
+ const lineNumbers = [];
2204
+ const prefix = this.getSchemaPrefix(library);
2205
+ if (!prefix) return lineNumbers;
2206
+ const lines = text.split("\n");
2207
+ let insideFunction = 0;
2208
+ for (let i = 0; i < lines.length; i++) {
2209
+ const line = lines[i] ?? "";
2210
+ const opens = (line.match(/\{/g) || []).length;
2211
+ const closes = (line.match(/\}/g) || []).length;
2212
+ if (/(?:function\s+\w+|=>)\s*\{/.test(line)) {
2213
+ insideFunction += opens;
2214
+ insideFunction -= closes;
2215
+ continue;
2216
+ }
2217
+ insideFunction += opens - closes;
2218
+ if (insideFunction > 0 && line.includes(prefix)) {
2219
+ lineNumbers.push(i + 1);
2220
+ }
2221
+ }
2222
+ return lineNumbers;
2223
+ }
2224
+ getSchemaPrefix(library) {
2225
+ switch (library) {
2226
+ case "zod":
2227
+ return "z.";
2228
+ case "yup":
2229
+ return "yup.";
2230
+ case "joi":
2231
+ return "Joi.";
2232
+ case "io-ts":
2233
+ return "t.";
2234
+ case "valibot":
2235
+ return "v.";
2236
+ default:
2237
+ return null;
2238
+ }
2239
+ }
1143
2240
  isSchemaExpression(text, library) {
1144
2241
  switch (library) {
1145
2242
  case "zod":
@@ -1159,16 +2256,16 @@ var GovernanceEngine = class {
1159
2256
  };
1160
2257
 
1161
2258
  // src/incremental.ts
1162
- import { existsSync as existsSync4, mkdirSync, readFileSync as readFileSync4, writeFileSync } from "fs";
1163
- import { join as join4 } from "path";
2259
+ import { existsSync as existsSync6, mkdirSync as mkdirSync2, readFileSync as readFileSync6, unlinkSync, writeFileSync as writeFileSync2 } from "fs";
2260
+ import { join as join6 } from "path";
1164
2261
  var STATE_DIR = ".schemashift";
1165
2262
  var STATE_FILE = "incremental.json";
1166
2263
  var IncrementalTracker = class {
1167
2264
  stateDir;
1168
2265
  statePath;
1169
2266
  constructor(projectPath) {
1170
- this.stateDir = join4(projectPath, STATE_DIR);
1171
- this.statePath = join4(this.stateDir, STATE_FILE);
2267
+ this.stateDir = join6(projectPath, STATE_DIR);
2268
+ this.statePath = join6(this.stateDir, STATE_FILE);
1172
2269
  }
1173
2270
  start(files, from, to) {
1174
2271
  const state = {
@@ -1203,9 +2300,9 @@ var IncrementalTracker = class {
1203
2300
  this.saveState(state);
1204
2301
  }
1205
2302
  getState() {
1206
- if (!existsSync4(this.statePath)) return null;
2303
+ if (!existsSync6(this.statePath)) return null;
1207
2304
  try {
1208
- return JSON.parse(readFileSync4(this.statePath, "utf-8"));
2305
+ return JSON.parse(readFileSync6(this.statePath, "utf-8"));
1209
2306
  } catch {
1210
2307
  return null;
1211
2308
  }
@@ -1232,21 +2329,21 @@ var IncrementalTracker = class {
1232
2329
  };
1233
2330
  }
1234
2331
  clear() {
1235
- if (existsSync4(this.statePath)) {
1236
- writeFileSync(this.statePath, "");
2332
+ if (existsSync6(this.statePath)) {
2333
+ unlinkSync(this.statePath);
1237
2334
  }
1238
2335
  }
1239
2336
  saveState(state) {
1240
- if (!existsSync4(this.stateDir)) {
1241
- mkdirSync(this.stateDir, { recursive: true });
2337
+ if (!existsSync6(this.stateDir)) {
2338
+ mkdirSync2(this.stateDir, { recursive: true });
1242
2339
  }
1243
- writeFileSync(this.statePath, JSON.stringify(state, null, 2));
2340
+ writeFileSync2(this.statePath, JSON.stringify(state, null, 2));
1244
2341
  }
1245
2342
  };
1246
2343
 
1247
2344
  // src/package-updater.ts
1248
- import { existsSync as existsSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync2 } from "fs";
1249
- import { join as join5 } from "path";
2345
+ import { existsSync as existsSync7, readFileSync as readFileSync7, writeFileSync as writeFileSync3 } from "fs";
2346
+ import { join as join7 } from "path";
1250
2347
  var TARGET_VERSIONS = {
1251
2348
  "yup->zod": { zod: "^3.24.0" },
1252
2349
  "joi->zod": { zod: "^3.24.0" },
@@ -1267,14 +2364,14 @@ var PackageUpdater = class {
1267
2364
  const add = {};
1268
2365
  const remove = [];
1269
2366
  const warnings = [];
1270
- const pkgPath = join5(projectPath, "package.json");
1271
- if (!existsSync5(pkgPath)) {
2367
+ const pkgPath = join7(projectPath, "package.json");
2368
+ if (!existsSync7(pkgPath)) {
1272
2369
  warnings.push("No package.json found. Cannot plan dependency updates.");
1273
2370
  return { add, remove, warnings };
1274
2371
  }
1275
2372
  let pkg;
1276
2373
  try {
1277
- pkg = JSON.parse(readFileSync5(pkgPath, "utf-8"));
2374
+ pkg = JSON.parse(readFileSync7(pkgPath, "utf-8"));
1278
2375
  } catch {
1279
2376
  warnings.push("Could not parse package.json.");
1280
2377
  return { add, remove, warnings };
@@ -1304,9 +2401,9 @@ var PackageUpdater = class {
1304
2401
  return { add, remove, warnings };
1305
2402
  }
1306
2403
  apply(projectPath, plan) {
1307
- const pkgPath = join5(projectPath, "package.json");
1308
- if (!existsSync5(pkgPath)) return;
1309
- const pkgText = readFileSync5(pkgPath, "utf-8");
2404
+ const pkgPath = join7(projectPath, "package.json");
2405
+ if (!existsSync7(pkgPath)) return;
2406
+ const pkgText = readFileSync7(pkgPath, "utf-8");
1310
2407
  const pkg = JSON.parse(pkgText);
1311
2408
  if (!pkg.dependencies) pkg.dependencies = {};
1312
2409
  for (const [name, version] of Object.entries(plan.add)) {
@@ -1316,11 +2413,133 @@ var PackageUpdater = class {
1316
2413
  pkg.dependencies[name] = version;
1317
2414
  }
1318
2415
  }
1319
- writeFileSync2(pkgPath, `${JSON.stringify(pkg, null, 2)}
2416
+ writeFileSync3(pkgPath, `${JSON.stringify(pkg, null, 2)}
1320
2417
  `);
1321
2418
  }
1322
2419
  };
1323
2420
 
2421
+ // src/performance-analyzer.ts
2422
+ var PerformanceAnalyzer = class {
2423
+ analyze(sourceFiles, from, to) {
2424
+ const warnings = [];
2425
+ let parseCallSites = 0;
2426
+ let dynamicSchemaCount = 0;
2427
+ for (const file of sourceFiles) {
2428
+ const text = file.getFullText();
2429
+ const filePath = file.getFilePath();
2430
+ const parseMatches = text.match(/\.(parse|safeParse)\s*\(/g);
2431
+ if (parseMatches) {
2432
+ parseCallSites += parseMatches.length;
2433
+ }
2434
+ const dynamicResult = this.detectDynamicSchemas(text, filePath);
2435
+ dynamicSchemaCount += dynamicResult.count;
2436
+ warnings.push(...dynamicResult.warnings);
2437
+ this.addMigrationWarnings(text, filePath, from, to, warnings);
2438
+ }
2439
+ const recommendation = this.getRecommendation(from, to, parseCallSites, dynamicSchemaCount);
2440
+ const summary = this.generateSummary(warnings, parseCallSites, dynamicSchemaCount);
2441
+ return {
2442
+ warnings,
2443
+ parseCallSites,
2444
+ dynamicSchemaCount,
2445
+ recommendation,
2446
+ summary
2447
+ };
2448
+ }
2449
+ detectDynamicSchemas(text, filePath) {
2450
+ const warnings = [];
2451
+ let count = 0;
2452
+ const functionBodyPattern = /(?:function\s+\w+\s*\([^)]*\)|const\s+\w+\s*=\s*(?:async\s+)?(?:\([^)]*\)|[a-zA-Z_]\w*)\s*=>)\s*\{[^}]*(?:z\.|yup\.|Joi\.|v\.)\w+\s*\(/g;
2453
+ for (const match of text.matchAll(functionBodyPattern)) {
2454
+ count++;
2455
+ const lineNumber = text.substring(0, match.index).split("\n").length;
2456
+ warnings.push({
2457
+ category: "dynamic-schemas",
2458
+ message: "Schema created inside function body \u2014 may cause performance issues with Zod v4.",
2459
+ detail: "Zod v4 uses JIT compilation, making schema creation ~17x slower than v3. Move schema definitions to module level to avoid re-creation on every call.",
2460
+ filePath,
2461
+ lineNumber,
2462
+ severity: "warning"
2463
+ });
2464
+ }
2465
+ const reactComponentPattern = /(?:function\s+[A-Z]\w*\s*\([^)]*\)|const\s+[A-Z]\w*\s*[:=])[^{]*\{[^}]*(?:z\.|yup\.|Joi\.)\w+\s*\(/g;
2466
+ for (const match of text.matchAll(reactComponentPattern)) {
2467
+ count++;
2468
+ const lineNumber = text.substring(0, match.index).split("\n").length;
2469
+ warnings.push({
2470
+ category: "schema-creation",
2471
+ message: "Schema appears to be created inside a React component.",
2472
+ detail: "Schemas created inside React components are re-created on every render. Move schema definitions outside the component or wrap in useMemo(). This is especially important for Zod v4 due to JIT compilation overhead.",
2473
+ filePath,
2474
+ lineNumber,
2475
+ severity: "warning"
2476
+ });
2477
+ }
2478
+ return { count, warnings };
2479
+ }
2480
+ addMigrationWarnings(text, filePath, from, to, warnings) {
2481
+ const migration = `${from}->${to}`;
2482
+ if (migration === "zod-v3->v4") {
2483
+ if (/edge-runtime|@vercel\/edge|cloudflare.*workers|deno\.serve|Deno\.serve/i.test(text) || /export\s+const\s+runtime\s*=\s*['"]edge['"]/i.test(text)) {
2484
+ warnings.push({
2485
+ category: "cold-start",
2486
+ message: "Edge/serverless environment detected \u2014 Zod v4 JIT compilation increases cold start time.",
2487
+ detail: "Zod v4 JIT trades slower schema creation for faster repeated parsing. In serverless/edge environments with short-lived instances, the JIT cost may not amortize. Consider Valibot or staying on Zod v3 for cold-start-sensitive code.",
2488
+ filePath,
2489
+ severity: "warning"
2490
+ });
2491
+ }
2492
+ const parseCount = (text.match(/\.parse\s*\(/g) || []).length;
2493
+ if (parseCount > 10) {
2494
+ warnings.push({
2495
+ category: "repeated-parsing",
2496
+ message: `High parse() usage (${parseCount} call sites) \u2014 Zod v4 JIT will benefit here.`,
2497
+ detail: "Zod v4 JIT compilation makes repeated parsing ~8x faster. This file has many parse() calls and will see performance improvement.",
2498
+ filePath,
2499
+ severity: "info"
2500
+ });
2501
+ }
2502
+ }
2503
+ if (migration === "zod->valibot" && /\.parse\s*\(/.test(text)) {
2504
+ warnings.push({
2505
+ category: "repeated-parsing",
2506
+ message: "Valibot parsing performance is comparable to Zod v4 for most schemas.",
2507
+ detail: "Valibot v1+ offers similar runtime performance to Zod v4 with significantly smaller bundle size. No JIT overhead means consistent performance across all environments.",
2508
+ filePath,
2509
+ severity: "info"
2510
+ });
2511
+ }
2512
+ }
2513
+ getRecommendation(from, to, parseCallSites, dynamicSchemaCount) {
2514
+ const migration = `${from}->${to}`;
2515
+ if (migration === "zod-v3->v4") {
2516
+ if (dynamicSchemaCount > 5) {
2517
+ return "Many dynamic schemas detected. Zod v4 JIT makes schema creation 17x slower. Move schemas to module level before migrating, or consider Valibot for size-sensitive apps.";
2518
+ }
2519
+ if (parseCallSites > 50) {
2520
+ return "High parse() volume detected. Zod v4 JIT will significantly benefit repeated parsing (up to 8x faster). Migration recommended for performance.";
2521
+ }
2522
+ return "Moderate usage detected. Zod v4 trades slower startup for faster runtime parsing.";
2523
+ }
2524
+ if (migration === "zod->valibot") {
2525
+ return "Valibot offers similar runtime performance with significantly smaller bundle size. Best suited for bundle-size-sensitive applications.";
2526
+ }
2527
+ if (from === "yup" || from === "joi") {
2528
+ return `Migrating from ${from} to ${to} should have neutral or positive performance impact.`;
2529
+ }
2530
+ return "Performance impact depends on usage patterns. Review warnings for details.";
2531
+ }
2532
+ generateSummary(warnings, parseCallSites, dynamicSchemaCount) {
2533
+ const parts = [];
2534
+ parts.push(`${parseCallSites} parse/safeParse call sites`);
2535
+ if (dynamicSchemaCount > 0) {
2536
+ parts.push(`${dynamicSchemaCount} dynamic schema creation sites`);
2537
+ }
2538
+ parts.push(`${warnings.length} performance warning(s)`);
2539
+ return parts.join(", ");
2540
+ }
2541
+ };
2542
+
1324
2543
  // src/plugin-loader.ts
1325
2544
  var PluginLoader = class {
1326
2545
  async loadPlugins(pluginPaths) {
@@ -1366,8 +2585,8 @@ var PluginLoader = class {
1366
2585
  };
1367
2586
 
1368
2587
  // src/standard-schema.ts
1369
- import { existsSync as existsSync6, readFileSync as readFileSync6 } from "fs";
1370
- import { join as join6 } from "path";
2588
+ import { existsSync as existsSync8, readFileSync as readFileSync8 } from "fs";
2589
+ import { join as join8 } from "path";
1371
2590
  var STANDARD_SCHEMA_LIBRARIES = {
1372
2591
  zod: { minMajor: 3, minMinor: 23 },
1373
2592
  // Zod v3.23+ and v4+
@@ -1396,16 +2615,16 @@ function isVersionCompatible(version, minMajor, minMinor) {
1396
2615
  return false;
1397
2616
  }
1398
2617
  function detectStandardSchema(projectPath) {
1399
- const pkgPath = join6(projectPath, "package.json");
1400
- if (!existsSync6(pkgPath)) {
1401
- return { detected: false, compatibleLibraries: [], recommendation: "" };
2618
+ const pkgPath = join8(projectPath, "package.json");
2619
+ if (!existsSync8(pkgPath)) {
2620
+ return { detected: false, compatibleLibraries: [], recommendation: "", interopTools: [] };
1402
2621
  }
1403
2622
  let allDeps = {};
1404
2623
  try {
1405
- const pkg = JSON.parse(readFileSync6(pkgPath, "utf-8"));
2624
+ const pkg = JSON.parse(readFileSync8(pkgPath, "utf-8"));
1406
2625
  allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
1407
2626
  } catch {
1408
- return { detected: false, compatibleLibraries: [], recommendation: "" };
2627
+ return { detected: false, compatibleLibraries: [], recommendation: "", interopTools: [] };
1409
2628
  }
1410
2629
  const hasExplicitStandardSchema = "@standard-schema/spec" in allDeps;
1411
2630
  const compatibleLibraries = [];
@@ -1424,9 +2643,155 @@ function detectStandardSchema(projectPath) {
1424
2643
  } else if (hasExplicitStandardSchema) {
1425
2644
  recommendation = "Standard Schema spec detected. Ensure your validation library supports Standard Schema for maximum interoperability.";
1426
2645
  }
1427
- return { detected, compatibleLibraries, recommendation };
2646
+ let adoptionPath;
2647
+ if (detected && !hasExplicitStandardSchema) {
2648
+ adoptionPath = "Install @standard-schema/spec for explicit Standard Schema support. This enables library-agnostic validation consumers to accept your schemas without depending on a specific library. Run: npm install @standard-schema/spec";
2649
+ } else if (!detected) {
2650
+ adoptionPath = "Consider migrating to a Standard Schema-compatible library (Zod v3.23+, Valibot v1+, ArkType v2+) to future-proof your validation layer and reduce library lock-in.";
2651
+ }
2652
+ const interopTools = detected ? [
2653
+ "tRPC v11+ (Standard Schema input validation)",
2654
+ "TanStack Form (schema-agnostic validation)",
2655
+ "TanStack Router (route parameter validation)",
2656
+ "Hono (request validation middleware)",
2657
+ "Conform (progressive form validation)",
2658
+ "Nuxt (runtime config validation)"
2659
+ ] : [];
2660
+ return { detected, compatibleLibraries, recommendation, adoptionPath, interopTools };
1428
2661
  }
1429
2662
 
2663
+ // src/test-scaffolder.ts
2664
+ var TestScaffolder = class {
2665
+ scaffold(sourceFiles, from, to) {
2666
+ const tests = [];
2667
+ let totalSchemas = 0;
2668
+ for (const file of sourceFiles) {
2669
+ const schemas = this.extractSchemaNames(file, from);
2670
+ if (schemas.length === 0) continue;
2671
+ totalSchemas += schemas.length;
2672
+ const testCode = this.generateTestFile(file, schemas, from, to);
2673
+ const filePath = file.getFilePath().replace(/\.tsx?$/, ".migration-test.ts");
2674
+ tests.push({ filePath, testCode, schemaCount: schemas.length });
2675
+ }
2676
+ const summary = tests.length > 0 ? `Generated ${tests.length} test file(s) covering ${totalSchemas} schema(s) for ${from}->${to} migration.` : "No schemas found to generate tests for.";
2677
+ return { tests, totalSchemas, summary };
2678
+ }
2679
+ extractSchemaNames(file, library) {
2680
+ const names = [];
2681
+ const prefixes = this.getLibraryPrefixes(library);
2682
+ for (const varDecl of file.getVariableDeclarations()) {
2683
+ const initializer = varDecl.getInitializer();
2684
+ if (!initializer) continue;
2685
+ const text = initializer.getText();
2686
+ if (prefixes.some((p) => text.startsWith(p))) {
2687
+ names.push(varDecl.getName());
2688
+ }
2689
+ }
2690
+ return names;
2691
+ }
2692
+ getLibraryPrefixes(library) {
2693
+ switch (library) {
2694
+ case "zod":
2695
+ case "zod-v3":
2696
+ return ["z.", "zod."];
2697
+ case "yup":
2698
+ return ["yup.", "Yup."];
2699
+ case "joi":
2700
+ return ["Joi.", "joi."];
2701
+ case "io-ts":
2702
+ return ["t."];
2703
+ case "valibot":
2704
+ return ["v.", "valibot."];
2705
+ default:
2706
+ return ["z."];
2707
+ }
2708
+ }
2709
+ generateTestFile(file, schemaNames, from, to) {
2710
+ const relativePath = file.getFilePath();
2711
+ const schemaImports = schemaNames.join(", ");
2712
+ const parseMethod = this.getParseMethod(to);
2713
+ const errorClass = this.getErrorClass(to);
2714
+ const testCases = schemaNames.map((name) => this.generateSchemaTests(name, to, parseMethod, errorClass)).join("\n\n");
2715
+ return `/**
2716
+ * Migration validation tests for ${from} -> ${to}
2717
+ * Auto-generated by SchemaShift
2718
+ *
2719
+ * These tests verify that schema behavior is preserved after migration.
2720
+ * Run before and after migration to ensure equivalence.
2721
+ *
2722
+ * Source: ${relativePath}
2723
+ */
2724
+ import { describe, expect, it } from 'vitest';
2725
+ import { ${schemaImports} } from '${relativePath.replace(/\.ts$/, ".js")}';
2726
+
2727
+ describe('Migration validation: ${relativePath}', () => {
2728
+ ${testCases}
2729
+ });
2730
+ `;
2731
+ }
2732
+ getParseMethod(to) {
2733
+ switch (to) {
2734
+ case "valibot":
2735
+ return "v.safeParse";
2736
+ default:
2737
+ return ".safeParse";
2738
+ }
2739
+ }
2740
+ getErrorClass(to) {
2741
+ switch (to) {
2742
+ case "valibot":
2743
+ return "ValiError";
2744
+ case "zod":
2745
+ case "v4":
2746
+ return "ZodError";
2747
+ default:
2748
+ return "Error";
2749
+ }
2750
+ }
2751
+ generateSchemaTests(schemaName, to, _parseMethod, _errorClass) {
2752
+ if (to === "valibot") {
2753
+ return ` describe('${schemaName}', () => {
2754
+ it('should accept valid data', () => {
2755
+ // TODO(schemashift): Add valid test data for ${schemaName}
2756
+ // const result = v.safeParse(${schemaName}, validData);
2757
+ // expect(result.success).toBe(true);
2758
+ });
2759
+
2760
+ it('should reject invalid data', () => {
2761
+ // TODO(schemashift): Add invalid test data for ${schemaName}
2762
+ // const result = v.safeParse(${schemaName}, invalidData);
2763
+ // expect(result.success).toBe(false);
2764
+ });
2765
+
2766
+ it('should preserve error messages', () => {
2767
+ // TODO(schemashift): Verify custom error messages are preserved
2768
+ // const result = v.safeParse(${schemaName}, invalidData);
2769
+ // expect(result.issues?.[0]?.message).toContain('expected message');
2770
+ });
2771
+ });`;
2772
+ }
2773
+ return ` describe('${schemaName}', () => {
2774
+ it('should accept valid data', () => {
2775
+ // TODO(schemashift): Add valid test data for ${schemaName}
2776
+ // const result = ${schemaName}.safeParse(validData);
2777
+ // expect(result.success).toBe(true);
2778
+ });
2779
+
2780
+ it('should reject invalid data', () => {
2781
+ // TODO(schemashift): Add invalid test data for ${schemaName}
2782
+ // const result = ${schemaName}.safeParse(invalidData);
2783
+ // expect(result.success).toBe(false);
2784
+ });
2785
+
2786
+ it('should preserve error messages', () => {
2787
+ // TODO(schemashift): Verify custom error messages are preserved
2788
+ // const result = ${schemaName}.safeParse(invalidData);
2789
+ // expect(result.error?.issues[0]?.message).toContain('expected message');
2790
+ });
2791
+ });`;
2792
+ }
2793
+ };
2794
+
1430
2795
  // src/transform.ts
1431
2796
  var TransformEngine = class {
1432
2797
  handlers = /* @__PURE__ */ new Map();
@@ -1441,9 +2806,10 @@ var TransformEngine = class {
1441
2806
  }
1442
2807
  getSupportedPaths() {
1443
2808
  return Array.from(this.handlers.keys()).map((key) => {
1444
- const [from, to] = key.split("->");
1445
- return { from, to };
1446
- });
2809
+ const parts = key.split("->");
2810
+ if (parts.length !== 2) return null;
2811
+ return { from: parts[0], to: parts[1] };
2812
+ }).filter((entry) => entry !== null);
1447
2813
  }
1448
2814
  transform(sourceFile, from, to, options) {
1449
2815
  const handler = this.getHandler(from, to);
@@ -1459,19 +2825,156 @@ var TransformEngine = class {
1459
2825
  return handler.transform(sourceFile, options);
1460
2826
  }
1461
2827
  };
2828
+
2829
+ // src/type-dedup-detector.ts
2830
+ import { Node } from "ts-morph";
2831
+ var TypeDedupDetector = class {
2832
+ detect(sourceFiles) {
2833
+ const typeDefinitions = this.collectTypeDefinitions(sourceFiles);
2834
+ const schemaDefinitions = this.collectSchemaDefinitions(sourceFiles);
2835
+ const candidates = this.findMatches(typeDefinitions, schemaDefinitions);
2836
+ const summary = candidates.length > 0 ? `Found ${candidates.length} type definition(s) that may duplicate schema shapes. After migration, replace with z.infer<typeof schema>.` : "No duplicate type definitions detected.";
2837
+ return { candidates, summary };
2838
+ }
2839
+ collectTypeDefinitions(sourceFiles) {
2840
+ const types = [];
2841
+ for (const file of sourceFiles) {
2842
+ const filePath = file.getFilePath();
2843
+ for (const iface of file.getInterfaces()) {
2844
+ const fields = iface.getProperties().map((p) => p.getName());
2845
+ if (fields.length > 0) {
2846
+ types.push({
2847
+ name: iface.getName(),
2848
+ fields,
2849
+ filePath,
2850
+ lineNumber: iface.getStartLineNumber()
2851
+ });
2852
+ }
2853
+ }
2854
+ for (const typeAlias of file.getTypeAliases()) {
2855
+ const typeNode = typeAlias.getTypeNode();
2856
+ if (!typeNode) continue;
2857
+ if (Node.isTypeLiteral(typeNode)) {
2858
+ const fields = typeNode.getProperties().map((p) => p.getName());
2859
+ if (fields.length > 0) {
2860
+ types.push({
2861
+ name: typeAlias.getName(),
2862
+ fields,
2863
+ filePath,
2864
+ lineNumber: typeAlias.getStartLineNumber()
2865
+ });
2866
+ }
2867
+ }
2868
+ }
2869
+ }
2870
+ return types;
2871
+ }
2872
+ collectSchemaDefinitions(sourceFiles) {
2873
+ const schemas = [];
2874
+ for (const file of sourceFiles) {
2875
+ const filePath = file.getFilePath();
2876
+ for (const varDecl of file.getVariableDeclarations()) {
2877
+ const initializer = varDecl.getInitializer();
2878
+ if (!initializer) continue;
2879
+ const text = initializer.getText();
2880
+ const isSchema = /(?:z|zod|yup|Yup|Joi|joi|t|v|valibot)\.object\s*\(/.test(text) || /Joi\.object\s*\(/.test(text);
2881
+ if (!isSchema) continue;
2882
+ const fields = this.extractSchemaFields(text);
2883
+ if (fields.length > 0) {
2884
+ schemas.push({
2885
+ name: varDecl.getName(),
2886
+ fields,
2887
+ filePath,
2888
+ lineNumber: varDecl.getStartLineNumber()
2889
+ });
2890
+ }
2891
+ }
2892
+ }
2893
+ return schemas;
2894
+ }
2895
+ extractSchemaFields(text) {
2896
+ const fields = [];
2897
+ const fieldPattern = /\b(\w+)\s*:\s*(?:z|zod|yup|Yup|Joi|joi|t|v|valibot)\./g;
2898
+ for (const match of text.matchAll(fieldPattern)) {
2899
+ if (match[1]) {
2900
+ fields.push(match[1]);
2901
+ }
2902
+ }
2903
+ return fields;
2904
+ }
2905
+ findMatches(types, schemas) {
2906
+ const candidates = [];
2907
+ for (const typeDef of types) {
2908
+ for (const schemaDef of schemas) {
2909
+ const matchedFields = this.getMatchedFields(typeDef.fields, schemaDef.fields);
2910
+ if (matchedFields.length < 2) continue;
2911
+ const typeFieldCount = typeDef.fields.length;
2912
+ const schemaFieldCount = schemaDef.fields.length;
2913
+ const matchRatio = matchedFields.length / Math.max(typeFieldCount, schemaFieldCount);
2914
+ let confidence;
2915
+ if (matchRatio >= 0.8) {
2916
+ confidence = "high";
2917
+ } else if (matchRatio >= 0.5) {
2918
+ confidence = "medium";
2919
+ } else {
2920
+ confidence = "low";
2921
+ }
2922
+ if (confidence === "low" && !this.namesRelated(typeDef.name, schemaDef.name)) {
2923
+ continue;
2924
+ }
2925
+ candidates.push({
2926
+ typeName: typeDef.name,
2927
+ typeFilePath: typeDef.filePath,
2928
+ typeLineNumber: typeDef.lineNumber,
2929
+ schemaName: schemaDef.name,
2930
+ schemaFilePath: schemaDef.filePath,
2931
+ schemaLineNumber: schemaDef.lineNumber,
2932
+ matchedFields,
2933
+ confidence,
2934
+ suggestion: `Replace "type/interface ${typeDef.name}" with "type ${typeDef.name} = z.infer<typeof ${schemaDef.name}>" (${matchedFields.length}/${typeFieldCount} fields match).`
2935
+ });
2936
+ }
2937
+ }
2938
+ candidates.sort((a, b) => {
2939
+ const confidenceOrder = { high: 0, medium: 1, low: 2 };
2940
+ const diff = confidenceOrder[a.confidence] - confidenceOrder[b.confidence];
2941
+ if (diff !== 0) return diff;
2942
+ return b.matchedFields.length - a.matchedFields.length;
2943
+ });
2944
+ return candidates;
2945
+ }
2946
+ getMatchedFields(typeFields, schemaFields) {
2947
+ const schemaSet = new Set(schemaFields);
2948
+ return typeFields.filter((f) => schemaSet.has(f));
2949
+ }
2950
+ namesRelated(typeName, schemaName) {
2951
+ const normalize = (name) => name.toLowerCase().replace(/schema|type|interface|i$/gi, "");
2952
+ return normalize(typeName) === normalize(schemaName);
2953
+ }
2954
+ };
1462
2955
  export {
2956
+ BehavioralWarningAnalyzer,
2957
+ BundleEstimator,
1463
2958
  CompatibilityAnalyzer,
2959
+ ComplexityEstimator,
1464
2960
  DetailedAnalyzer,
1465
2961
  EcosystemAnalyzer,
2962
+ FormResolverMigrator,
1466
2963
  GovernanceEngine,
1467
2964
  IncrementalTracker,
2965
+ MigrationAuditLog,
1468
2966
  MigrationChain,
2967
+ MonorepoResolver,
1469
2968
  PackageUpdater,
2969
+ PerformanceAnalyzer,
1470
2970
  PluginLoader,
1471
2971
  SchemaAnalyzer,
1472
2972
  SchemaDependencyResolver,
2973
+ TestScaffolder,
1473
2974
  TransformEngine,
2975
+ TypeDedupDetector,
1474
2976
  buildCallChain,
2977
+ computeParallelBatches,
1475
2978
  detectFormLibraries,
1476
2979
  detectSchemaLibrary,
1477
2980
  detectStandardSchema,