@schemashift/core 0.9.0 → 0.10.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
@@ -10,6 +10,9 @@ var LIBRARY_PATTERNS = {
10
10
  joi: [/^joi$/, /^@hapi\/joi$/],
11
11
  "io-ts": [/^io-ts$/, /^io-ts\//],
12
12
  valibot: [/^valibot$/],
13
+ arktype: [/^arktype$/],
14
+ superstruct: [/^superstruct$/],
15
+ effect: [/^@effect\/schema$/],
13
16
  v4: [],
14
17
  // Target version, not detectable from imports
15
18
  unknown: []
@@ -304,7 +307,8 @@ var MigrationAuditLog = class {
304
307
  errorCount: params.errorCount,
305
308
  riskScore: params.riskScore,
306
309
  duration: params.duration,
307
- user: this.getCurrentUser()
310
+ user: this.getCurrentUser(),
311
+ metadata: params.metadata || this.collectMetadata()
308
312
  };
309
313
  }
310
314
  /**
@@ -346,12 +350,67 @@ var MigrationAuditLog = class {
346
350
  migrationPaths
347
351
  };
348
352
  }
353
+ /**
354
+ * Export audit log as JSON string.
355
+ */
356
+ exportJson() {
357
+ const log = this.read();
358
+ return JSON.stringify(log, null, 2);
359
+ }
360
+ /**
361
+ * Export audit log as CSV string.
362
+ */
363
+ exportCsv() {
364
+ const log = this.read();
365
+ const headers = [
366
+ "timestamp",
367
+ "migrationId",
368
+ "filePath",
369
+ "action",
370
+ "from",
371
+ "to",
372
+ "success",
373
+ "warningCount",
374
+ "errorCount",
375
+ "riskScore",
376
+ "user",
377
+ "duration"
378
+ ];
379
+ const rows = log.entries.map(
380
+ (e) => headers.map((h) => {
381
+ const val = e[h];
382
+ if (val === void 0 || val === null) return "";
383
+ return String(val).includes(",") ? `"${String(val)}"` : String(val);
384
+ }).join(",")
385
+ );
386
+ return [headers.join(","), ...rows].join("\n");
387
+ }
388
+ /**
389
+ * Get entries filtered by date range.
390
+ */
391
+ getByDateRange(start, end) {
392
+ const log = this.read();
393
+ return log.entries.filter((e) => {
394
+ const ts = new Date(e.timestamp);
395
+ return ts >= start && ts <= end;
396
+ });
397
+ }
349
398
  /**
350
399
  * Clear the audit log.
351
400
  */
352
401
  clear() {
353
402
  this.write({ version: AUDIT_VERSION, entries: [] });
354
403
  }
404
+ collectMetadata() {
405
+ return {
406
+ hostname: process.env.HOSTNAME || void 0,
407
+ nodeVersion: process.version,
408
+ ciJobId: process.env.CI_JOB_ID || process.env.GITHUB_RUN_ID || void 0,
409
+ ciProvider: process.env.GITHUB_ACTIONS ? "github" : process.env.GITLAB_CI ? "gitlab" : process.env.CIRCLECI ? "circleci" : process.env.JENKINS_URL ? "jenkins" : void 0,
410
+ gitBranch: process.env.GITHUB_REF_NAME || process.env.CI_COMMIT_BRANCH || void 0,
411
+ gitCommit: process.env.GITHUB_SHA || process.env.CI_COMMIT_SHA || void 0
412
+ };
413
+ }
355
414
  write(log) {
356
415
  if (!existsSync(this.logDir)) {
357
416
  mkdirSync(this.logDir, { recursive: true });
@@ -970,6 +1029,125 @@ var ECOSYSTEM_RULES = [
970
1029
  severity: "error"
971
1030
  })
972
1031
  },
1032
+ // Zod-based HTTP/API clients
1033
+ {
1034
+ package: "zodios",
1035
+ category: "api",
1036
+ migrations: ["zod-v3->v4"],
1037
+ check: () => ({
1038
+ issue: "Zodios uses Zod schemas for API contract definitions. Zod v4 type changes may break contracts.",
1039
+ suggestion: "Upgrade Zodios to a Zod v4-compatible version and verify all API contracts.",
1040
+ severity: "warning",
1041
+ upgradeCommand: "npm install @zodios/core@latest"
1042
+ })
1043
+ },
1044
+ {
1045
+ package: "@zodios/core",
1046
+ category: "api",
1047
+ migrations: ["zod-v3->v4"],
1048
+ check: () => ({
1049
+ issue: "@zodios/core uses Zod schemas for API contract definitions. Zod v4 type changes may break contracts.",
1050
+ suggestion: "Upgrade @zodios/core to a Zod v4-compatible version and verify all API contracts.",
1051
+ severity: "warning",
1052
+ upgradeCommand: "npm install @zodios/core@latest"
1053
+ })
1054
+ },
1055
+ {
1056
+ package: "@ts-rest/core",
1057
+ category: "api",
1058
+ migrations: ["zod-v3->v4"],
1059
+ check: () => ({
1060
+ issue: "@ts-rest/core uses Zod for contract definitions. Zod v4 type incompatibilities may break runtime validation.",
1061
+ suggestion: "Upgrade @ts-rest/core to a version with Zod v4 support.",
1062
+ severity: "warning",
1063
+ upgradeCommand: "npm install @ts-rest/core@latest"
1064
+ })
1065
+ },
1066
+ {
1067
+ package: "trpc-openapi",
1068
+ category: "openapi",
1069
+ migrations: ["zod-v3->v4"],
1070
+ check: () => ({
1071
+ issue: "trpc-openapi needs a v4-compatible version for Zod v4.",
1072
+ suggestion: "Check for a Zod v4-compatible version of trpc-openapi before upgrading.",
1073
+ severity: "warning",
1074
+ upgradeCommand: "npm install trpc-openapi@latest"
1075
+ })
1076
+ },
1077
+ // Form data and URL state libraries
1078
+ {
1079
+ package: "zod-form-data",
1080
+ category: "form",
1081
+ migrations: ["zod-v3->v4"],
1082
+ check: () => ({
1083
+ issue: "zod-form-data relies on Zod v3 internals (_def) which moved to _zod.def in v4.",
1084
+ suggestion: "Upgrade zod-form-data to a Zod v4-compatible version.",
1085
+ severity: "error",
1086
+ upgradeCommand: "npm install zod-form-data@latest"
1087
+ })
1088
+ },
1089
+ {
1090
+ package: "@conform-to/zod",
1091
+ category: "form",
1092
+ migrations: ["zod-v3->v4"],
1093
+ check: () => ({
1094
+ issue: "@conform-to/zod may have Zod v4 compatibility issues.",
1095
+ suggestion: "Upgrade @conform-to/zod to the latest version with Zod v4 support.",
1096
+ severity: "warning",
1097
+ upgradeCommand: "npm install @conform-to/zod@latest"
1098
+ })
1099
+ },
1100
+ {
1101
+ package: "nuqs",
1102
+ category: "validation-util",
1103
+ migrations: ["zod-v3->v4"],
1104
+ check: () => ({
1105
+ issue: "nuqs uses Zod for URL state parsing. Zod v4 changes may affect URL parameter validation.",
1106
+ suggestion: "Upgrade nuqs to a version with Zod v4 support.",
1107
+ severity: "warning",
1108
+ upgradeCommand: "npm install nuqs@latest"
1109
+ })
1110
+ },
1111
+ // Schema library detection for cross-library migrations
1112
+ {
1113
+ package: "@effect/schema",
1114
+ category: "validation-util",
1115
+ migrations: ["io-ts->zod"],
1116
+ check: () => ({
1117
+ issue: "@effect/schema detected \u2014 this is the successor to io-ts/fp-ts. Consider migrating to Effect Schema instead of Zod if you prefer FP patterns.",
1118
+ suggestion: "If using fp-ts patterns heavily, consider Effect Schema as the migration target instead of Zod.",
1119
+ severity: "info"
1120
+ })
1121
+ },
1122
+ {
1123
+ package: "arktype",
1124
+ category: "validation-util",
1125
+ migrations: ["zod->valibot", "zod-v3->v4"],
1126
+ check: (_version, migration) => {
1127
+ if (migration === "zod->valibot") {
1128
+ return {
1129
+ issue: "ArkType detected alongside Zod. Consider ArkType as a migration target \u2014 it offers 100x faster validation and Standard Schema support.",
1130
+ suggestion: "Consider migrating to ArkType for performance-critical paths, or keep Zod for ecosystem compatibility.",
1131
+ severity: "info"
1132
+ };
1133
+ }
1134
+ return {
1135
+ issue: "ArkType detected alongside Zod. ArkType supports Standard Schema, making it interoperable with Zod v4.",
1136
+ suggestion: "No action needed \u2014 ArkType and Zod v4 can coexist via Standard Schema.",
1137
+ severity: "info"
1138
+ };
1139
+ }
1140
+ },
1141
+ {
1142
+ package: "superstruct",
1143
+ category: "validation-util",
1144
+ migrations: ["yup->zod", "joi->zod"],
1145
+ check: () => ({
1146
+ issue: "Superstruct detected in the project. Consider migrating Superstruct schemas to Zod as well for a unified validation approach.",
1147
+ suggestion: "Use SchemaShift to migrate Superstruct schemas alongside Yup/Joi schemas.",
1148
+ severity: "info"
1149
+ })
1150
+ },
973
1151
  // Additional validation utilities
974
1152
  {
975
1153
  package: "zod-to-json-schema",
@@ -1894,6 +2072,165 @@ var DetailedAnalyzer = class {
1894
2072
  }
1895
2073
  };
1896
2074
 
2075
+ // src/drift-detector.ts
2076
+ import { createHash as createHash2 } from "crypto";
2077
+ import { existsSync as existsSync6, mkdirSync as mkdirSync2, readFileSync as readFileSync6, writeFileSync as writeFileSync2 } from "fs";
2078
+ import { join as join6, relative } from "path";
2079
+ var SNAPSHOT_DIR = ".schemashift";
2080
+ var SNAPSHOT_FILE = "schema-snapshot.json";
2081
+ var SNAPSHOT_VERSION = 1;
2082
+ var DriftDetector = class {
2083
+ snapshotDir;
2084
+ snapshotPath;
2085
+ constructor(projectPath) {
2086
+ this.snapshotDir = join6(projectPath, SNAPSHOT_DIR);
2087
+ this.snapshotPath = join6(this.snapshotDir, SNAPSHOT_FILE);
2088
+ }
2089
+ /**
2090
+ * Take a snapshot of the current schema state
2091
+ */
2092
+ snapshot(files, projectPath) {
2093
+ const schemas = [];
2094
+ for (const filePath of files) {
2095
+ if (!existsSync6(filePath)) continue;
2096
+ const content = readFileSync6(filePath, "utf-8");
2097
+ const library = this.detectLibraryFromContent(content);
2098
+ if (library === "unknown") continue;
2099
+ const schemaNames = this.extractSchemaNames(content);
2100
+ schemas.push({
2101
+ filePath: relative(projectPath, filePath),
2102
+ library,
2103
+ contentHash: this.hashContent(content),
2104
+ schemaCount: schemaNames.length,
2105
+ schemaNames
2106
+ });
2107
+ }
2108
+ const snapshot = {
2109
+ version: SNAPSHOT_VERSION,
2110
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2111
+ projectPath,
2112
+ schemas
2113
+ };
2114
+ return snapshot;
2115
+ }
2116
+ /**
2117
+ * Save a snapshot to disk
2118
+ */
2119
+ saveSnapshot(snapshot) {
2120
+ if (!existsSync6(this.snapshotDir)) {
2121
+ mkdirSync2(this.snapshotDir, { recursive: true });
2122
+ }
2123
+ writeFileSync2(this.snapshotPath, JSON.stringify(snapshot, null, 2));
2124
+ }
2125
+ /**
2126
+ * Load saved snapshot from disk
2127
+ */
2128
+ loadSnapshot() {
2129
+ if (!existsSync6(this.snapshotPath)) {
2130
+ return null;
2131
+ }
2132
+ try {
2133
+ const content = readFileSync6(this.snapshotPath, "utf-8");
2134
+ return JSON.parse(content);
2135
+ } catch {
2136
+ return null;
2137
+ }
2138
+ }
2139
+ /**
2140
+ * Compare current state against saved snapshot
2141
+ */
2142
+ detect(currentFiles, projectPath) {
2143
+ const saved = this.loadSnapshot();
2144
+ if (!saved) {
2145
+ return {
2146
+ hasDrift: false,
2147
+ added: [],
2148
+ removed: [],
2149
+ modified: [],
2150
+ unchanged: 0,
2151
+ totalFiles: 0,
2152
+ snapshotTimestamp: ""
2153
+ };
2154
+ }
2155
+ const current = this.snapshot(currentFiles, projectPath);
2156
+ return this.compareSnapshots(saved, current);
2157
+ }
2158
+ /**
2159
+ * Compare two snapshots and return drift results
2160
+ */
2161
+ compareSnapshots(baseline, current) {
2162
+ const baselineMap = new Map(baseline.schemas.map((s) => [s.filePath, s]));
2163
+ const currentMap = new Map(current.schemas.map((s) => [s.filePath, s]));
2164
+ const added = [];
2165
+ const removed = [];
2166
+ const modified = [];
2167
+ let unchanged = 0;
2168
+ for (const [path, currentFile] of currentMap) {
2169
+ const baselineFile = baselineMap.get(path);
2170
+ if (!baselineFile) {
2171
+ added.push(currentFile);
2172
+ } else if (currentFile.contentHash !== baselineFile.contentHash) {
2173
+ const addedSchemas = currentFile.schemaNames.filter(
2174
+ (n) => !baselineFile.schemaNames.includes(n)
2175
+ );
2176
+ const removedSchemas = baselineFile.schemaNames.filter(
2177
+ (n) => !currentFile.schemaNames.includes(n)
2178
+ );
2179
+ modified.push({
2180
+ filePath: path,
2181
+ library: currentFile.library,
2182
+ previousHash: baselineFile.contentHash,
2183
+ currentHash: currentFile.contentHash,
2184
+ previousSchemaCount: baselineFile.schemaCount,
2185
+ currentSchemaCount: currentFile.schemaCount,
2186
+ addedSchemas,
2187
+ removedSchemas
2188
+ });
2189
+ } else {
2190
+ unchanged++;
2191
+ }
2192
+ }
2193
+ for (const [path, baselineFile] of baselineMap) {
2194
+ if (!currentMap.has(path)) {
2195
+ removed.push(baselineFile);
2196
+ }
2197
+ }
2198
+ return {
2199
+ hasDrift: added.length > 0 || removed.length > 0 || modified.length > 0,
2200
+ added,
2201
+ removed,
2202
+ modified,
2203
+ unchanged,
2204
+ totalFiles: currentMap.size,
2205
+ snapshotTimestamp: baseline.timestamp
2206
+ };
2207
+ }
2208
+ extractSchemaNames(content) {
2209
+ const names = [];
2210
+ const pattern = /(?:const|let|var)\s+(\w+)\s*=\s*(?:z\.|yup\.|Joi\.|t\.|v\.|type\(|object\(|string\(|S\.)/g;
2211
+ for (const match of content.matchAll(pattern)) {
2212
+ if (match[1]) names.push(match[1]);
2213
+ }
2214
+ return names;
2215
+ }
2216
+ detectLibraryFromContent(content) {
2217
+ if (/from\s*['"]zod['"]/.test(content) || /\bz\./.test(content)) return "zod";
2218
+ if (/from\s*['"]yup['"]/.test(content) || /\byup\./.test(content)) return "yup";
2219
+ if (/from\s*['"]joi['"]/.test(content) || /\bJoi\./.test(content)) return "joi";
2220
+ if (/from\s*['"]io-ts['"]/.test(content) || /\bt\./.test(content) && /from\s*['"]io-ts/.test(content))
2221
+ return "io-ts";
2222
+ if (/from\s*['"]valibot['"]/.test(content) || /\bv\./.test(content) && /from\s*['"]valibot/.test(content))
2223
+ return "valibot";
2224
+ if (/from\s*['"]arktype['"]/.test(content)) return "arktype";
2225
+ if (/from\s*['"]superstruct['"]/.test(content)) return "superstruct";
2226
+ if (/from\s*['"]@effect\/schema['"]/.test(content)) return "effect";
2227
+ return "unknown";
2228
+ }
2229
+ hashContent(content) {
2230
+ return createHash2("sha256").update(content).digest("hex").substring(0, 16);
2231
+ }
2232
+ };
2233
+
1897
2234
  // src/form-resolver-migrator.ts
1898
2235
  var RESOLVER_MAPPINGS = {
1899
2236
  "yup->zod": [
@@ -2255,17 +2592,265 @@ var GovernanceEngine = class {
2255
2592
  }
2256
2593
  };
2257
2594
 
2595
+ // src/governance-templates.ts
2596
+ var GOVERNANCE_TEMPLATES = [
2597
+ {
2598
+ name: "no-any-schemas",
2599
+ description: "Disallow z.any(), yup.mixed() without constraints, and similar unrestricted types",
2600
+ category: "security",
2601
+ rule: (sourceFile, _config) => {
2602
+ const violations = [];
2603
+ const text = sourceFile.getFullText();
2604
+ const filePath = sourceFile.getFilePath();
2605
+ const lines = text.split("\n");
2606
+ const anyPatterns = [
2607
+ /\bz\.any\(\)/,
2608
+ /\byup\.mixed\(\)/,
2609
+ /\bt\.any\b/,
2610
+ /\bv\.any\(\)/,
2611
+ /\bunknown\(\)/
2612
+ ];
2613
+ for (let i = 0; i < lines.length; i++) {
2614
+ const line = lines[i] ?? "";
2615
+ for (const pattern of anyPatterns) {
2616
+ if (pattern.test(line)) {
2617
+ violations.push({
2618
+ rule: "no-any-schemas",
2619
+ message: "Unrestricted type (any/mixed/unknown) found. Use a specific type with constraints.",
2620
+ filePath,
2621
+ lineNumber: i + 1,
2622
+ schemaName: "",
2623
+ severity: "error",
2624
+ fixable: false
2625
+ });
2626
+ }
2627
+ }
2628
+ }
2629
+ return violations;
2630
+ }
2631
+ },
2632
+ {
2633
+ name: "require-descriptions",
2634
+ description: "All exported schemas must have .describe() for documentation",
2635
+ category: "quality",
2636
+ rule: (sourceFile, _config) => {
2637
+ const violations = [];
2638
+ const text = sourceFile.getFullText();
2639
+ const filePath = sourceFile.getFilePath();
2640
+ const lines = text.split("\n");
2641
+ for (let i = 0; i < lines.length; i++) {
2642
+ const line = lines[i] ?? "";
2643
+ if (/export\s+(const|let)\s+\w+.*=\s*(z\.|yup\.)/.test(line)) {
2644
+ let fullStatement = line;
2645
+ let j = i + 1;
2646
+ while (j < lines.length && !lines[j]?.includes(";") && j < i + 10) {
2647
+ fullStatement += lines[j] ?? "";
2648
+ j++;
2649
+ }
2650
+ if (j < lines.length) fullStatement += lines[j] ?? "";
2651
+ if (!fullStatement.includes(".describe(")) {
2652
+ const nameMatch = line.match(/(?:const|let)\s+(\w+)/);
2653
+ violations.push({
2654
+ rule: "require-descriptions",
2655
+ message: `Exported schema ${nameMatch?.[1] || "unknown"} should include .describe() for documentation.`,
2656
+ filePath,
2657
+ lineNumber: i + 1,
2658
+ schemaName: nameMatch?.[1] || "",
2659
+ severity: "warning",
2660
+ fixable: true
2661
+ });
2662
+ }
2663
+ }
2664
+ }
2665
+ return violations;
2666
+ }
2667
+ },
2668
+ {
2669
+ name: "max-nesting-depth",
2670
+ description: "Limit schema nesting depth to prevent TypeScript performance issues",
2671
+ category: "performance",
2672
+ rule: (sourceFile, config) => {
2673
+ const violations = [];
2674
+ const text = sourceFile.getFullText();
2675
+ const filePath = sourceFile.getFilePath();
2676
+ const maxDepth = config.threshold || 5;
2677
+ const lines = text.split("\n");
2678
+ let currentDepth = 0;
2679
+ let maxFoundDepth = 0;
2680
+ let deepestLine = 0;
2681
+ for (let i = 0; i < lines.length; i++) {
2682
+ const line = lines[i] ?? "";
2683
+ for (const char of line) {
2684
+ if (char === "(" || char === "{" || char === "[") {
2685
+ currentDepth++;
2686
+ if (currentDepth > maxFoundDepth) {
2687
+ maxFoundDepth = currentDepth;
2688
+ deepestLine = i + 1;
2689
+ }
2690
+ }
2691
+ if (char === ")" || char === "}" || char === "]") {
2692
+ currentDepth = Math.max(0, currentDepth - 1);
2693
+ }
2694
+ }
2695
+ }
2696
+ if (maxFoundDepth > maxDepth) {
2697
+ violations.push({
2698
+ rule: "max-nesting-depth",
2699
+ message: `Schema nesting depth ${maxFoundDepth} exceeds maximum of ${maxDepth}. Consider breaking into smaller schemas.`,
2700
+ filePath,
2701
+ lineNumber: deepestLine,
2702
+ schemaName: "",
2703
+ severity: "warning",
2704
+ fixable: false
2705
+ });
2706
+ }
2707
+ return violations;
2708
+ }
2709
+ },
2710
+ {
2711
+ name: "no-deprecated-methods",
2712
+ description: "Flag usage of deprecated schema methods",
2713
+ category: "quality",
2714
+ rule: (sourceFile, _config) => {
2715
+ const violations = [];
2716
+ const text = sourceFile.getFullText();
2717
+ const filePath = sourceFile.getFilePath();
2718
+ const lines = text.split("\n");
2719
+ const deprecatedPatterns = [
2720
+ {
2721
+ pattern: /\.deepPartial\(\)/,
2722
+ message: ".deepPartial() is removed in Zod v4. Use recursive .partial() instead."
2723
+ },
2724
+ {
2725
+ pattern: /\.strip\(\)/,
2726
+ message: ".strip() is deprecated. Use z.strictObject() or explicit stripping."
2727
+ },
2728
+ {
2729
+ pattern: /z\.promise\(/,
2730
+ message: "z.promise() is deprecated in Zod v4. Use native Promise types."
2731
+ },
2732
+ {
2733
+ pattern: /z\.ostring\(\)/,
2734
+ message: "z.ostring() is removed in Zod v4. Use z.string().optional()."
2735
+ },
2736
+ {
2737
+ pattern: /z\.onumber\(\)/,
2738
+ message: "z.onumber() is removed in Zod v4. Use z.number().optional()."
2739
+ },
2740
+ {
2741
+ pattern: /z\.oboolean\(\)/,
2742
+ message: "z.oboolean() is removed in Zod v4. Use z.boolean().optional()."
2743
+ },
2744
+ {
2745
+ pattern: /z\.preprocess\(/,
2746
+ message: "z.preprocess() is removed in Zod v4. Use z.coerce.* instead."
2747
+ }
2748
+ ];
2749
+ for (let i = 0; i < lines.length; i++) {
2750
+ const line = lines[i] ?? "";
2751
+ for (const { pattern, message } of deprecatedPatterns) {
2752
+ if (pattern.test(line)) {
2753
+ violations.push({
2754
+ rule: "no-deprecated-methods",
2755
+ message,
2756
+ filePath,
2757
+ lineNumber: i + 1,
2758
+ schemaName: "",
2759
+ severity: "warning",
2760
+ fixable: false
2761
+ });
2762
+ }
2763
+ }
2764
+ }
2765
+ return violations;
2766
+ }
2767
+ },
2768
+ {
2769
+ name: "naming-convention",
2770
+ description: "Enforce schema naming pattern (e.g., must end with Schema)",
2771
+ category: "quality",
2772
+ rule: (sourceFile, config) => {
2773
+ const violations = [];
2774
+ const text = sourceFile.getFullText();
2775
+ const filePath = sourceFile.getFilePath();
2776
+ const lines = text.split("\n");
2777
+ const pattern = new RegExp(config.pattern || ".*Schema$");
2778
+ for (let i = 0; i < lines.length; i++) {
2779
+ const line = lines[i] ?? "";
2780
+ const match = line.match(
2781
+ /(?:const|let)\s+(\w+)\s*=\s*(?:z\.|yup\.|Joi\.|t\.|v\.|type\(|object\(|string\()/
2782
+ );
2783
+ if (match?.[1] && !pattern.test(match[1])) {
2784
+ violations.push({
2785
+ rule: "naming-convention",
2786
+ message: `Schema "${match[1]}" does not match naming pattern ${pattern.source}.`,
2787
+ filePath,
2788
+ lineNumber: i + 1,
2789
+ schemaName: match[1],
2790
+ severity: "warning",
2791
+ fixable: false
2792
+ });
2793
+ }
2794
+ }
2795
+ return violations;
2796
+ }
2797
+ },
2798
+ {
2799
+ name: "require-max-length",
2800
+ description: "String schemas must have .max() to prevent DoS via unbounded input",
2801
+ category: "security",
2802
+ rule: (sourceFile, _config) => {
2803
+ const violations = [];
2804
+ const text = sourceFile.getFullText();
2805
+ const filePath = sourceFile.getFilePath();
2806
+ const lines = text.split("\n");
2807
+ for (let i = 0; i < lines.length; i++) {
2808
+ const line = lines[i] ?? "";
2809
+ if (/z\.string\(\)/.test(line) && !line.includes(".max(") && !line.includes(".length(")) {
2810
+ let fullChain = line;
2811
+ let j = i + 1;
2812
+ while (j < lines.length && j < i + 5 && /^\s*\./.test(lines[j] ?? "")) {
2813
+ fullChain += lines[j] ?? "";
2814
+ j++;
2815
+ }
2816
+ if (!fullChain.includes(".max(") && !fullChain.includes(".length(")) {
2817
+ violations.push({
2818
+ rule: "require-max-length",
2819
+ message: "String schema should have .max() to prevent unbounded input (DoS protection).",
2820
+ filePath,
2821
+ lineNumber: i + 1,
2822
+ schemaName: "",
2823
+ severity: "warning",
2824
+ fixable: true
2825
+ });
2826
+ }
2827
+ }
2828
+ }
2829
+ return violations;
2830
+ }
2831
+ }
2832
+ ];
2833
+ function getGovernanceTemplate(name) {
2834
+ return GOVERNANCE_TEMPLATES.find((t) => t.name === name);
2835
+ }
2836
+ function getGovernanceTemplatesByCategory(category) {
2837
+ return GOVERNANCE_TEMPLATES.filter((t) => t.category === category);
2838
+ }
2839
+ function getGovernanceTemplateNames() {
2840
+ return GOVERNANCE_TEMPLATES.map((t) => t.name);
2841
+ }
2842
+
2258
2843
  // src/incremental.ts
2259
- import { existsSync as existsSync6, mkdirSync as mkdirSync2, readFileSync as readFileSync6, unlinkSync, writeFileSync as writeFileSync2 } from "fs";
2260
- import { join as join6 } from "path";
2844
+ import { existsSync as existsSync7, mkdirSync as mkdirSync3, readFileSync as readFileSync7, unlinkSync, writeFileSync as writeFileSync3 } from "fs";
2845
+ import { join as join7 } from "path";
2261
2846
  var STATE_DIR = ".schemashift";
2262
2847
  var STATE_FILE = "incremental.json";
2263
2848
  var IncrementalTracker = class {
2264
2849
  stateDir;
2265
2850
  statePath;
2266
2851
  constructor(projectPath) {
2267
- this.stateDir = join6(projectPath, STATE_DIR);
2268
- this.statePath = join6(this.stateDir, STATE_FILE);
2852
+ this.stateDir = join7(projectPath, STATE_DIR);
2853
+ this.statePath = join7(this.stateDir, STATE_FILE);
2269
2854
  }
2270
2855
  start(files, from, to) {
2271
2856
  const state = {
@@ -2300,9 +2885,9 @@ var IncrementalTracker = class {
2300
2885
  this.saveState(state);
2301
2886
  }
2302
2887
  getState() {
2303
- if (!existsSync6(this.statePath)) return null;
2888
+ if (!existsSync7(this.statePath)) return null;
2304
2889
  try {
2305
- return JSON.parse(readFileSync6(this.statePath, "utf-8"));
2890
+ return JSON.parse(readFileSync7(this.statePath, "utf-8"));
2306
2891
  } catch {
2307
2892
  return null;
2308
2893
  }
@@ -2329,21 +2914,21 @@ var IncrementalTracker = class {
2329
2914
  };
2330
2915
  }
2331
2916
  clear() {
2332
- if (existsSync6(this.statePath)) {
2917
+ if (existsSync7(this.statePath)) {
2333
2918
  unlinkSync(this.statePath);
2334
2919
  }
2335
2920
  }
2336
2921
  saveState(state) {
2337
- if (!existsSync6(this.stateDir)) {
2338
- mkdirSync2(this.stateDir, { recursive: true });
2922
+ if (!existsSync7(this.stateDir)) {
2923
+ mkdirSync3(this.stateDir, { recursive: true });
2339
2924
  }
2340
- writeFileSync2(this.statePath, JSON.stringify(state, null, 2));
2925
+ writeFileSync3(this.statePath, JSON.stringify(state, null, 2));
2341
2926
  }
2342
2927
  };
2343
2928
 
2344
2929
  // src/package-updater.ts
2345
- import { existsSync as existsSync7, readFileSync as readFileSync7, writeFileSync as writeFileSync3 } from "fs";
2346
- import { join as join7 } from "path";
2930
+ import { existsSync as existsSync8, readFileSync as readFileSync8, writeFileSync as writeFileSync4 } from "fs";
2931
+ import { join as join8 } from "path";
2347
2932
  var TARGET_VERSIONS = {
2348
2933
  "yup->zod": { zod: "^3.24.0" },
2349
2934
  "joi->zod": { zod: "^3.24.0" },
@@ -2364,14 +2949,14 @@ var PackageUpdater = class {
2364
2949
  const add = {};
2365
2950
  const remove = [];
2366
2951
  const warnings = [];
2367
- const pkgPath = join7(projectPath, "package.json");
2368
- if (!existsSync7(pkgPath)) {
2952
+ const pkgPath = join8(projectPath, "package.json");
2953
+ if (!existsSync8(pkgPath)) {
2369
2954
  warnings.push("No package.json found. Cannot plan dependency updates.");
2370
2955
  return { add, remove, warnings };
2371
2956
  }
2372
2957
  let pkg;
2373
2958
  try {
2374
- pkg = JSON.parse(readFileSync7(pkgPath, "utf-8"));
2959
+ pkg = JSON.parse(readFileSync8(pkgPath, "utf-8"));
2375
2960
  } catch {
2376
2961
  warnings.push("Could not parse package.json.");
2377
2962
  return { add, remove, warnings };
@@ -2401,9 +2986,9 @@ var PackageUpdater = class {
2401
2986
  return { add, remove, warnings };
2402
2987
  }
2403
2988
  apply(projectPath, plan) {
2404
- const pkgPath = join7(projectPath, "package.json");
2405
- if (!existsSync7(pkgPath)) return;
2406
- const pkgText = readFileSync7(pkgPath, "utf-8");
2989
+ const pkgPath = join8(projectPath, "package.json");
2990
+ if (!existsSync8(pkgPath)) return;
2991
+ const pkgText = readFileSync8(pkgPath, "utf-8");
2407
2992
  const pkg = JSON.parse(pkgText);
2408
2993
  if (!pkg.dependencies) pkg.dependencies = {};
2409
2994
  for (const [name, version] of Object.entries(plan.add)) {
@@ -2413,7 +2998,7 @@ var PackageUpdater = class {
2413
2998
  pkg.dependencies[name] = version;
2414
2999
  }
2415
3000
  }
2416
- writeFileSync3(pkgPath, `${JSON.stringify(pkg, null, 2)}
3001
+ writeFileSync4(pkgPath, `${JSON.stringify(pkg, null, 2)}
2417
3002
  `);
2418
3003
  }
2419
3004
  };
@@ -2585,8 +3170,8 @@ var PluginLoader = class {
2585
3170
  };
2586
3171
 
2587
3172
  // src/standard-schema.ts
2588
- import { existsSync as existsSync8, readFileSync as readFileSync8 } from "fs";
2589
- import { join as join8 } from "path";
3173
+ import { existsSync as existsSync9, readFileSync as readFileSync9 } from "fs";
3174
+ import { join as join9 } from "path";
2590
3175
  var STANDARD_SCHEMA_LIBRARIES = {
2591
3176
  zod: { minMajor: 3, minMinor: 23 },
2592
3177
  // Zod v3.23+ and v4+
@@ -2615,13 +3200,13 @@ function isVersionCompatible(version, minMajor, minMinor) {
2615
3200
  return false;
2616
3201
  }
2617
3202
  function detectStandardSchema(projectPath) {
2618
- const pkgPath = join8(projectPath, "package.json");
2619
- if (!existsSync8(pkgPath)) {
3203
+ const pkgPath = join9(projectPath, "package.json");
3204
+ if (!existsSync9(pkgPath)) {
2620
3205
  return { detected: false, compatibleLibraries: [], recommendation: "", interopTools: [] };
2621
3206
  }
2622
3207
  let allDeps = {};
2623
3208
  try {
2624
- const pkg = JSON.parse(readFileSync8(pkgPath, "utf-8"));
3209
+ const pkg = JSON.parse(readFileSync9(pkgPath, "utf-8"));
2625
3210
  allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
2626
3211
  } catch {
2627
3212
  return { detected: false, compatibleLibraries: [], recommendation: "", interopTools: [] };
@@ -2958,8 +3543,10 @@ export {
2958
3543
  CompatibilityAnalyzer,
2959
3544
  ComplexityEstimator,
2960
3545
  DetailedAnalyzer,
3546
+ DriftDetector,
2961
3547
  EcosystemAnalyzer,
2962
3548
  FormResolverMigrator,
3549
+ GOVERNANCE_TEMPLATES,
2963
3550
  GovernanceEngine,
2964
3551
  IncrementalTracker,
2965
3552
  MigrationAuditLog,
@@ -2978,6 +3565,9 @@ export {
2978
3565
  detectFormLibraries,
2979
3566
  detectSchemaLibrary,
2980
3567
  detectStandardSchema,
3568
+ getGovernanceTemplate,
3569
+ getGovernanceTemplateNames,
3570
+ getGovernanceTemplatesByCategory,
2981
3571
  isInsideComment,
2982
3572
  isInsideStringLiteral,
2983
3573
  loadConfig,