@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.cjs +621 -26
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +122 -2
- package/dist/index.d.ts +122 -2
- package/dist/index.js +615 -25
- package/dist/index.js.map +1 -1
- package/package.json +6 -1
package/dist/index.cjs
CHANGED
|
@@ -25,8 +25,10 @@ __export(index_exports, {
|
|
|
25
25
|
CompatibilityAnalyzer: () => CompatibilityAnalyzer,
|
|
26
26
|
ComplexityEstimator: () => ComplexityEstimator,
|
|
27
27
|
DetailedAnalyzer: () => DetailedAnalyzer,
|
|
28
|
+
DriftDetector: () => DriftDetector,
|
|
28
29
|
EcosystemAnalyzer: () => EcosystemAnalyzer,
|
|
29
30
|
FormResolverMigrator: () => FormResolverMigrator,
|
|
31
|
+
GOVERNANCE_TEMPLATES: () => GOVERNANCE_TEMPLATES,
|
|
30
32
|
GovernanceEngine: () => GovernanceEngine,
|
|
31
33
|
IncrementalTracker: () => IncrementalTracker,
|
|
32
34
|
MigrationAuditLog: () => MigrationAuditLog,
|
|
@@ -45,6 +47,9 @@ __export(index_exports, {
|
|
|
45
47
|
detectFormLibraries: () => detectFormLibraries,
|
|
46
48
|
detectSchemaLibrary: () => detectSchemaLibrary,
|
|
47
49
|
detectStandardSchema: () => detectStandardSchema,
|
|
50
|
+
getGovernanceTemplate: () => getGovernanceTemplate,
|
|
51
|
+
getGovernanceTemplateNames: () => getGovernanceTemplateNames,
|
|
52
|
+
getGovernanceTemplatesByCategory: () => getGovernanceTemplatesByCategory,
|
|
48
53
|
isInsideComment: () => isInsideComment,
|
|
49
54
|
isInsideStringLiteral: () => isInsideStringLiteral,
|
|
50
55
|
loadConfig: () => loadConfig,
|
|
@@ -68,6 +73,9 @@ var LIBRARY_PATTERNS = {
|
|
|
68
73
|
joi: [/^joi$/, /^@hapi\/joi$/],
|
|
69
74
|
"io-ts": [/^io-ts$/, /^io-ts\//],
|
|
70
75
|
valibot: [/^valibot$/],
|
|
76
|
+
arktype: [/^arktype$/],
|
|
77
|
+
superstruct: [/^superstruct$/],
|
|
78
|
+
effect: [/^@effect\/schema$/],
|
|
71
79
|
v4: [],
|
|
72
80
|
// Target version, not detectable from imports
|
|
73
81
|
unknown: []
|
|
@@ -362,7 +370,8 @@ var MigrationAuditLog = class {
|
|
|
362
370
|
errorCount: params.errorCount,
|
|
363
371
|
riskScore: params.riskScore,
|
|
364
372
|
duration: params.duration,
|
|
365
|
-
user: this.getCurrentUser()
|
|
373
|
+
user: this.getCurrentUser(),
|
|
374
|
+
metadata: params.metadata || this.collectMetadata()
|
|
366
375
|
};
|
|
367
376
|
}
|
|
368
377
|
/**
|
|
@@ -404,12 +413,67 @@ var MigrationAuditLog = class {
|
|
|
404
413
|
migrationPaths
|
|
405
414
|
};
|
|
406
415
|
}
|
|
416
|
+
/**
|
|
417
|
+
* Export audit log as JSON string.
|
|
418
|
+
*/
|
|
419
|
+
exportJson() {
|
|
420
|
+
const log = this.read();
|
|
421
|
+
return JSON.stringify(log, null, 2);
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
424
|
+
* Export audit log as CSV string.
|
|
425
|
+
*/
|
|
426
|
+
exportCsv() {
|
|
427
|
+
const log = this.read();
|
|
428
|
+
const headers = [
|
|
429
|
+
"timestamp",
|
|
430
|
+
"migrationId",
|
|
431
|
+
"filePath",
|
|
432
|
+
"action",
|
|
433
|
+
"from",
|
|
434
|
+
"to",
|
|
435
|
+
"success",
|
|
436
|
+
"warningCount",
|
|
437
|
+
"errorCount",
|
|
438
|
+
"riskScore",
|
|
439
|
+
"user",
|
|
440
|
+
"duration"
|
|
441
|
+
];
|
|
442
|
+
const rows = log.entries.map(
|
|
443
|
+
(e) => headers.map((h) => {
|
|
444
|
+
const val = e[h];
|
|
445
|
+
if (val === void 0 || val === null) return "";
|
|
446
|
+
return String(val).includes(",") ? `"${String(val)}"` : String(val);
|
|
447
|
+
}).join(",")
|
|
448
|
+
);
|
|
449
|
+
return [headers.join(","), ...rows].join("\n");
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* Get entries filtered by date range.
|
|
453
|
+
*/
|
|
454
|
+
getByDateRange(start, end) {
|
|
455
|
+
const log = this.read();
|
|
456
|
+
return log.entries.filter((e) => {
|
|
457
|
+
const ts = new Date(e.timestamp);
|
|
458
|
+
return ts >= start && ts <= end;
|
|
459
|
+
});
|
|
460
|
+
}
|
|
407
461
|
/**
|
|
408
462
|
* Clear the audit log.
|
|
409
463
|
*/
|
|
410
464
|
clear() {
|
|
411
465
|
this.write({ version: AUDIT_VERSION, entries: [] });
|
|
412
466
|
}
|
|
467
|
+
collectMetadata() {
|
|
468
|
+
return {
|
|
469
|
+
hostname: process.env.HOSTNAME || void 0,
|
|
470
|
+
nodeVersion: process.version,
|
|
471
|
+
ciJobId: process.env.CI_JOB_ID || process.env.GITHUB_RUN_ID || void 0,
|
|
472
|
+
ciProvider: process.env.GITHUB_ACTIONS ? "github" : process.env.GITLAB_CI ? "gitlab" : process.env.CIRCLECI ? "circleci" : process.env.JENKINS_URL ? "jenkins" : void 0,
|
|
473
|
+
gitBranch: process.env.GITHUB_REF_NAME || process.env.CI_COMMIT_BRANCH || void 0,
|
|
474
|
+
gitCommit: process.env.GITHUB_SHA || process.env.CI_COMMIT_SHA || void 0
|
|
475
|
+
};
|
|
476
|
+
}
|
|
413
477
|
write(log) {
|
|
414
478
|
if (!(0, import_node_fs.existsSync)(this.logDir)) {
|
|
415
479
|
(0, import_node_fs.mkdirSync)(this.logDir, { recursive: true });
|
|
@@ -1028,6 +1092,125 @@ var ECOSYSTEM_RULES = [
|
|
|
1028
1092
|
severity: "error"
|
|
1029
1093
|
})
|
|
1030
1094
|
},
|
|
1095
|
+
// Zod-based HTTP/API clients
|
|
1096
|
+
{
|
|
1097
|
+
package: "zodios",
|
|
1098
|
+
category: "api",
|
|
1099
|
+
migrations: ["zod-v3->v4"],
|
|
1100
|
+
check: () => ({
|
|
1101
|
+
issue: "Zodios uses Zod schemas for API contract definitions. Zod v4 type changes may break contracts.",
|
|
1102
|
+
suggestion: "Upgrade Zodios to a Zod v4-compatible version and verify all API contracts.",
|
|
1103
|
+
severity: "warning",
|
|
1104
|
+
upgradeCommand: "npm install @zodios/core@latest"
|
|
1105
|
+
})
|
|
1106
|
+
},
|
|
1107
|
+
{
|
|
1108
|
+
package: "@zodios/core",
|
|
1109
|
+
category: "api",
|
|
1110
|
+
migrations: ["zod-v3->v4"],
|
|
1111
|
+
check: () => ({
|
|
1112
|
+
issue: "@zodios/core uses Zod schemas for API contract definitions. Zod v4 type changes may break contracts.",
|
|
1113
|
+
suggestion: "Upgrade @zodios/core to a Zod v4-compatible version and verify all API contracts.",
|
|
1114
|
+
severity: "warning",
|
|
1115
|
+
upgradeCommand: "npm install @zodios/core@latest"
|
|
1116
|
+
})
|
|
1117
|
+
},
|
|
1118
|
+
{
|
|
1119
|
+
package: "@ts-rest/core",
|
|
1120
|
+
category: "api",
|
|
1121
|
+
migrations: ["zod-v3->v4"],
|
|
1122
|
+
check: () => ({
|
|
1123
|
+
issue: "@ts-rest/core uses Zod for contract definitions. Zod v4 type incompatibilities may break runtime validation.",
|
|
1124
|
+
suggestion: "Upgrade @ts-rest/core to a version with Zod v4 support.",
|
|
1125
|
+
severity: "warning",
|
|
1126
|
+
upgradeCommand: "npm install @ts-rest/core@latest"
|
|
1127
|
+
})
|
|
1128
|
+
},
|
|
1129
|
+
{
|
|
1130
|
+
package: "trpc-openapi",
|
|
1131
|
+
category: "openapi",
|
|
1132
|
+
migrations: ["zod-v3->v4"],
|
|
1133
|
+
check: () => ({
|
|
1134
|
+
issue: "trpc-openapi needs a v4-compatible version for Zod v4.",
|
|
1135
|
+
suggestion: "Check for a Zod v4-compatible version of trpc-openapi before upgrading.",
|
|
1136
|
+
severity: "warning",
|
|
1137
|
+
upgradeCommand: "npm install trpc-openapi@latest"
|
|
1138
|
+
})
|
|
1139
|
+
},
|
|
1140
|
+
// Form data and URL state libraries
|
|
1141
|
+
{
|
|
1142
|
+
package: "zod-form-data",
|
|
1143
|
+
category: "form",
|
|
1144
|
+
migrations: ["zod-v3->v4"],
|
|
1145
|
+
check: () => ({
|
|
1146
|
+
issue: "zod-form-data relies on Zod v3 internals (_def) which moved to _zod.def in v4.",
|
|
1147
|
+
suggestion: "Upgrade zod-form-data to a Zod v4-compatible version.",
|
|
1148
|
+
severity: "error",
|
|
1149
|
+
upgradeCommand: "npm install zod-form-data@latest"
|
|
1150
|
+
})
|
|
1151
|
+
},
|
|
1152
|
+
{
|
|
1153
|
+
package: "@conform-to/zod",
|
|
1154
|
+
category: "form",
|
|
1155
|
+
migrations: ["zod-v3->v4"],
|
|
1156
|
+
check: () => ({
|
|
1157
|
+
issue: "@conform-to/zod may have Zod v4 compatibility issues.",
|
|
1158
|
+
suggestion: "Upgrade @conform-to/zod to the latest version with Zod v4 support.",
|
|
1159
|
+
severity: "warning",
|
|
1160
|
+
upgradeCommand: "npm install @conform-to/zod@latest"
|
|
1161
|
+
})
|
|
1162
|
+
},
|
|
1163
|
+
{
|
|
1164
|
+
package: "nuqs",
|
|
1165
|
+
category: "validation-util",
|
|
1166
|
+
migrations: ["zod-v3->v4"],
|
|
1167
|
+
check: () => ({
|
|
1168
|
+
issue: "nuqs uses Zod for URL state parsing. Zod v4 changes may affect URL parameter validation.",
|
|
1169
|
+
suggestion: "Upgrade nuqs to a version with Zod v4 support.",
|
|
1170
|
+
severity: "warning",
|
|
1171
|
+
upgradeCommand: "npm install nuqs@latest"
|
|
1172
|
+
})
|
|
1173
|
+
},
|
|
1174
|
+
// Schema library detection for cross-library migrations
|
|
1175
|
+
{
|
|
1176
|
+
package: "@effect/schema",
|
|
1177
|
+
category: "validation-util",
|
|
1178
|
+
migrations: ["io-ts->zod"],
|
|
1179
|
+
check: () => ({
|
|
1180
|
+
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.",
|
|
1181
|
+
suggestion: "If using fp-ts patterns heavily, consider Effect Schema as the migration target instead of Zod.",
|
|
1182
|
+
severity: "info"
|
|
1183
|
+
})
|
|
1184
|
+
},
|
|
1185
|
+
{
|
|
1186
|
+
package: "arktype",
|
|
1187
|
+
category: "validation-util",
|
|
1188
|
+
migrations: ["zod->valibot", "zod-v3->v4"],
|
|
1189
|
+
check: (_version, migration) => {
|
|
1190
|
+
if (migration === "zod->valibot") {
|
|
1191
|
+
return {
|
|
1192
|
+
issue: "ArkType detected alongside Zod. Consider ArkType as a migration target \u2014 it offers 100x faster validation and Standard Schema support.",
|
|
1193
|
+
suggestion: "Consider migrating to ArkType for performance-critical paths, or keep Zod for ecosystem compatibility.",
|
|
1194
|
+
severity: "info"
|
|
1195
|
+
};
|
|
1196
|
+
}
|
|
1197
|
+
return {
|
|
1198
|
+
issue: "ArkType detected alongside Zod. ArkType supports Standard Schema, making it interoperable with Zod v4.",
|
|
1199
|
+
suggestion: "No action needed \u2014 ArkType and Zod v4 can coexist via Standard Schema.",
|
|
1200
|
+
severity: "info"
|
|
1201
|
+
};
|
|
1202
|
+
}
|
|
1203
|
+
},
|
|
1204
|
+
{
|
|
1205
|
+
package: "superstruct",
|
|
1206
|
+
category: "validation-util",
|
|
1207
|
+
migrations: ["yup->zod", "joi->zod"],
|
|
1208
|
+
check: () => ({
|
|
1209
|
+
issue: "Superstruct detected in the project. Consider migrating Superstruct schemas to Zod as well for a unified validation approach.",
|
|
1210
|
+
suggestion: "Use SchemaShift to migrate Superstruct schemas alongside Yup/Joi schemas.",
|
|
1211
|
+
severity: "info"
|
|
1212
|
+
})
|
|
1213
|
+
},
|
|
1031
1214
|
// Additional validation utilities
|
|
1032
1215
|
{
|
|
1033
1216
|
package: "zod-to-json-schema",
|
|
@@ -1952,6 +2135,165 @@ var DetailedAnalyzer = class {
|
|
|
1952
2135
|
}
|
|
1953
2136
|
};
|
|
1954
2137
|
|
|
2138
|
+
// src/drift-detector.ts
|
|
2139
|
+
var import_node_crypto2 = require("crypto");
|
|
2140
|
+
var import_node_fs6 = require("fs");
|
|
2141
|
+
var import_node_path6 = require("path");
|
|
2142
|
+
var SNAPSHOT_DIR = ".schemashift";
|
|
2143
|
+
var SNAPSHOT_FILE = "schema-snapshot.json";
|
|
2144
|
+
var SNAPSHOT_VERSION = 1;
|
|
2145
|
+
var DriftDetector = class {
|
|
2146
|
+
snapshotDir;
|
|
2147
|
+
snapshotPath;
|
|
2148
|
+
constructor(projectPath) {
|
|
2149
|
+
this.snapshotDir = (0, import_node_path6.join)(projectPath, SNAPSHOT_DIR);
|
|
2150
|
+
this.snapshotPath = (0, import_node_path6.join)(this.snapshotDir, SNAPSHOT_FILE);
|
|
2151
|
+
}
|
|
2152
|
+
/**
|
|
2153
|
+
* Take a snapshot of the current schema state
|
|
2154
|
+
*/
|
|
2155
|
+
snapshot(files, projectPath) {
|
|
2156
|
+
const schemas = [];
|
|
2157
|
+
for (const filePath of files) {
|
|
2158
|
+
if (!(0, import_node_fs6.existsSync)(filePath)) continue;
|
|
2159
|
+
const content = (0, import_node_fs6.readFileSync)(filePath, "utf-8");
|
|
2160
|
+
const library = this.detectLibraryFromContent(content);
|
|
2161
|
+
if (library === "unknown") continue;
|
|
2162
|
+
const schemaNames = this.extractSchemaNames(content);
|
|
2163
|
+
schemas.push({
|
|
2164
|
+
filePath: (0, import_node_path6.relative)(projectPath, filePath),
|
|
2165
|
+
library,
|
|
2166
|
+
contentHash: this.hashContent(content),
|
|
2167
|
+
schemaCount: schemaNames.length,
|
|
2168
|
+
schemaNames
|
|
2169
|
+
});
|
|
2170
|
+
}
|
|
2171
|
+
const snapshot = {
|
|
2172
|
+
version: SNAPSHOT_VERSION,
|
|
2173
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2174
|
+
projectPath,
|
|
2175
|
+
schemas
|
|
2176
|
+
};
|
|
2177
|
+
return snapshot;
|
|
2178
|
+
}
|
|
2179
|
+
/**
|
|
2180
|
+
* Save a snapshot to disk
|
|
2181
|
+
*/
|
|
2182
|
+
saveSnapshot(snapshot) {
|
|
2183
|
+
if (!(0, import_node_fs6.existsSync)(this.snapshotDir)) {
|
|
2184
|
+
(0, import_node_fs6.mkdirSync)(this.snapshotDir, { recursive: true });
|
|
2185
|
+
}
|
|
2186
|
+
(0, import_node_fs6.writeFileSync)(this.snapshotPath, JSON.stringify(snapshot, null, 2));
|
|
2187
|
+
}
|
|
2188
|
+
/**
|
|
2189
|
+
* Load saved snapshot from disk
|
|
2190
|
+
*/
|
|
2191
|
+
loadSnapshot() {
|
|
2192
|
+
if (!(0, import_node_fs6.existsSync)(this.snapshotPath)) {
|
|
2193
|
+
return null;
|
|
2194
|
+
}
|
|
2195
|
+
try {
|
|
2196
|
+
const content = (0, import_node_fs6.readFileSync)(this.snapshotPath, "utf-8");
|
|
2197
|
+
return JSON.parse(content);
|
|
2198
|
+
} catch {
|
|
2199
|
+
return null;
|
|
2200
|
+
}
|
|
2201
|
+
}
|
|
2202
|
+
/**
|
|
2203
|
+
* Compare current state against saved snapshot
|
|
2204
|
+
*/
|
|
2205
|
+
detect(currentFiles, projectPath) {
|
|
2206
|
+
const saved = this.loadSnapshot();
|
|
2207
|
+
if (!saved) {
|
|
2208
|
+
return {
|
|
2209
|
+
hasDrift: false,
|
|
2210
|
+
added: [],
|
|
2211
|
+
removed: [],
|
|
2212
|
+
modified: [],
|
|
2213
|
+
unchanged: 0,
|
|
2214
|
+
totalFiles: 0,
|
|
2215
|
+
snapshotTimestamp: ""
|
|
2216
|
+
};
|
|
2217
|
+
}
|
|
2218
|
+
const current = this.snapshot(currentFiles, projectPath);
|
|
2219
|
+
return this.compareSnapshots(saved, current);
|
|
2220
|
+
}
|
|
2221
|
+
/**
|
|
2222
|
+
* Compare two snapshots and return drift results
|
|
2223
|
+
*/
|
|
2224
|
+
compareSnapshots(baseline, current) {
|
|
2225
|
+
const baselineMap = new Map(baseline.schemas.map((s) => [s.filePath, s]));
|
|
2226
|
+
const currentMap = new Map(current.schemas.map((s) => [s.filePath, s]));
|
|
2227
|
+
const added = [];
|
|
2228
|
+
const removed = [];
|
|
2229
|
+
const modified = [];
|
|
2230
|
+
let unchanged = 0;
|
|
2231
|
+
for (const [path, currentFile] of currentMap) {
|
|
2232
|
+
const baselineFile = baselineMap.get(path);
|
|
2233
|
+
if (!baselineFile) {
|
|
2234
|
+
added.push(currentFile);
|
|
2235
|
+
} else if (currentFile.contentHash !== baselineFile.contentHash) {
|
|
2236
|
+
const addedSchemas = currentFile.schemaNames.filter(
|
|
2237
|
+
(n) => !baselineFile.schemaNames.includes(n)
|
|
2238
|
+
);
|
|
2239
|
+
const removedSchemas = baselineFile.schemaNames.filter(
|
|
2240
|
+
(n) => !currentFile.schemaNames.includes(n)
|
|
2241
|
+
);
|
|
2242
|
+
modified.push({
|
|
2243
|
+
filePath: path,
|
|
2244
|
+
library: currentFile.library,
|
|
2245
|
+
previousHash: baselineFile.contentHash,
|
|
2246
|
+
currentHash: currentFile.contentHash,
|
|
2247
|
+
previousSchemaCount: baselineFile.schemaCount,
|
|
2248
|
+
currentSchemaCount: currentFile.schemaCount,
|
|
2249
|
+
addedSchemas,
|
|
2250
|
+
removedSchemas
|
|
2251
|
+
});
|
|
2252
|
+
} else {
|
|
2253
|
+
unchanged++;
|
|
2254
|
+
}
|
|
2255
|
+
}
|
|
2256
|
+
for (const [path, baselineFile] of baselineMap) {
|
|
2257
|
+
if (!currentMap.has(path)) {
|
|
2258
|
+
removed.push(baselineFile);
|
|
2259
|
+
}
|
|
2260
|
+
}
|
|
2261
|
+
return {
|
|
2262
|
+
hasDrift: added.length > 0 || removed.length > 0 || modified.length > 0,
|
|
2263
|
+
added,
|
|
2264
|
+
removed,
|
|
2265
|
+
modified,
|
|
2266
|
+
unchanged,
|
|
2267
|
+
totalFiles: currentMap.size,
|
|
2268
|
+
snapshotTimestamp: baseline.timestamp
|
|
2269
|
+
};
|
|
2270
|
+
}
|
|
2271
|
+
extractSchemaNames(content) {
|
|
2272
|
+
const names = [];
|
|
2273
|
+
const pattern = /(?:const|let|var)\s+(\w+)\s*=\s*(?:z\.|yup\.|Joi\.|t\.|v\.|type\(|object\(|string\(|S\.)/g;
|
|
2274
|
+
for (const match of content.matchAll(pattern)) {
|
|
2275
|
+
if (match[1]) names.push(match[1]);
|
|
2276
|
+
}
|
|
2277
|
+
return names;
|
|
2278
|
+
}
|
|
2279
|
+
detectLibraryFromContent(content) {
|
|
2280
|
+
if (/from\s*['"]zod['"]/.test(content) || /\bz\./.test(content)) return "zod";
|
|
2281
|
+
if (/from\s*['"]yup['"]/.test(content) || /\byup\./.test(content)) return "yup";
|
|
2282
|
+
if (/from\s*['"]joi['"]/.test(content) || /\bJoi\./.test(content)) return "joi";
|
|
2283
|
+
if (/from\s*['"]io-ts['"]/.test(content) || /\bt\./.test(content) && /from\s*['"]io-ts/.test(content))
|
|
2284
|
+
return "io-ts";
|
|
2285
|
+
if (/from\s*['"]valibot['"]/.test(content) || /\bv\./.test(content) && /from\s*['"]valibot/.test(content))
|
|
2286
|
+
return "valibot";
|
|
2287
|
+
if (/from\s*['"]arktype['"]/.test(content)) return "arktype";
|
|
2288
|
+
if (/from\s*['"]superstruct['"]/.test(content)) return "superstruct";
|
|
2289
|
+
if (/from\s*['"]@effect\/schema['"]/.test(content)) return "effect";
|
|
2290
|
+
return "unknown";
|
|
2291
|
+
}
|
|
2292
|
+
hashContent(content) {
|
|
2293
|
+
return (0, import_node_crypto2.createHash)("sha256").update(content).digest("hex").substring(0, 16);
|
|
2294
|
+
}
|
|
2295
|
+
};
|
|
2296
|
+
|
|
1955
2297
|
// src/form-resolver-migrator.ts
|
|
1956
2298
|
var RESOLVER_MAPPINGS = {
|
|
1957
2299
|
"yup->zod": [
|
|
@@ -2313,17 +2655,265 @@ var GovernanceEngine = class {
|
|
|
2313
2655
|
}
|
|
2314
2656
|
};
|
|
2315
2657
|
|
|
2658
|
+
// src/governance-templates.ts
|
|
2659
|
+
var GOVERNANCE_TEMPLATES = [
|
|
2660
|
+
{
|
|
2661
|
+
name: "no-any-schemas",
|
|
2662
|
+
description: "Disallow z.any(), yup.mixed() without constraints, and similar unrestricted types",
|
|
2663
|
+
category: "security",
|
|
2664
|
+
rule: (sourceFile, _config) => {
|
|
2665
|
+
const violations = [];
|
|
2666
|
+
const text = sourceFile.getFullText();
|
|
2667
|
+
const filePath = sourceFile.getFilePath();
|
|
2668
|
+
const lines = text.split("\n");
|
|
2669
|
+
const anyPatterns = [
|
|
2670
|
+
/\bz\.any\(\)/,
|
|
2671
|
+
/\byup\.mixed\(\)/,
|
|
2672
|
+
/\bt\.any\b/,
|
|
2673
|
+
/\bv\.any\(\)/,
|
|
2674
|
+
/\bunknown\(\)/
|
|
2675
|
+
];
|
|
2676
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2677
|
+
const line = lines[i] ?? "";
|
|
2678
|
+
for (const pattern of anyPatterns) {
|
|
2679
|
+
if (pattern.test(line)) {
|
|
2680
|
+
violations.push({
|
|
2681
|
+
rule: "no-any-schemas",
|
|
2682
|
+
message: "Unrestricted type (any/mixed/unknown) found. Use a specific type with constraints.",
|
|
2683
|
+
filePath,
|
|
2684
|
+
lineNumber: i + 1,
|
|
2685
|
+
schemaName: "",
|
|
2686
|
+
severity: "error",
|
|
2687
|
+
fixable: false
|
|
2688
|
+
});
|
|
2689
|
+
}
|
|
2690
|
+
}
|
|
2691
|
+
}
|
|
2692
|
+
return violations;
|
|
2693
|
+
}
|
|
2694
|
+
},
|
|
2695
|
+
{
|
|
2696
|
+
name: "require-descriptions",
|
|
2697
|
+
description: "All exported schemas must have .describe() for documentation",
|
|
2698
|
+
category: "quality",
|
|
2699
|
+
rule: (sourceFile, _config) => {
|
|
2700
|
+
const violations = [];
|
|
2701
|
+
const text = sourceFile.getFullText();
|
|
2702
|
+
const filePath = sourceFile.getFilePath();
|
|
2703
|
+
const lines = text.split("\n");
|
|
2704
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2705
|
+
const line = lines[i] ?? "";
|
|
2706
|
+
if (/export\s+(const|let)\s+\w+.*=\s*(z\.|yup\.)/.test(line)) {
|
|
2707
|
+
let fullStatement = line;
|
|
2708
|
+
let j = i + 1;
|
|
2709
|
+
while (j < lines.length && !lines[j]?.includes(";") && j < i + 10) {
|
|
2710
|
+
fullStatement += lines[j] ?? "";
|
|
2711
|
+
j++;
|
|
2712
|
+
}
|
|
2713
|
+
if (j < lines.length) fullStatement += lines[j] ?? "";
|
|
2714
|
+
if (!fullStatement.includes(".describe(")) {
|
|
2715
|
+
const nameMatch = line.match(/(?:const|let)\s+(\w+)/);
|
|
2716
|
+
violations.push({
|
|
2717
|
+
rule: "require-descriptions",
|
|
2718
|
+
message: `Exported schema ${nameMatch?.[1] || "unknown"} should include .describe() for documentation.`,
|
|
2719
|
+
filePath,
|
|
2720
|
+
lineNumber: i + 1,
|
|
2721
|
+
schemaName: nameMatch?.[1] || "",
|
|
2722
|
+
severity: "warning",
|
|
2723
|
+
fixable: true
|
|
2724
|
+
});
|
|
2725
|
+
}
|
|
2726
|
+
}
|
|
2727
|
+
}
|
|
2728
|
+
return violations;
|
|
2729
|
+
}
|
|
2730
|
+
},
|
|
2731
|
+
{
|
|
2732
|
+
name: "max-nesting-depth",
|
|
2733
|
+
description: "Limit schema nesting depth to prevent TypeScript performance issues",
|
|
2734
|
+
category: "performance",
|
|
2735
|
+
rule: (sourceFile, config) => {
|
|
2736
|
+
const violations = [];
|
|
2737
|
+
const text = sourceFile.getFullText();
|
|
2738
|
+
const filePath = sourceFile.getFilePath();
|
|
2739
|
+
const maxDepth = config.threshold || 5;
|
|
2740
|
+
const lines = text.split("\n");
|
|
2741
|
+
let currentDepth = 0;
|
|
2742
|
+
let maxFoundDepth = 0;
|
|
2743
|
+
let deepestLine = 0;
|
|
2744
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2745
|
+
const line = lines[i] ?? "";
|
|
2746
|
+
for (const char of line) {
|
|
2747
|
+
if (char === "(" || char === "{" || char === "[") {
|
|
2748
|
+
currentDepth++;
|
|
2749
|
+
if (currentDepth > maxFoundDepth) {
|
|
2750
|
+
maxFoundDepth = currentDepth;
|
|
2751
|
+
deepestLine = i + 1;
|
|
2752
|
+
}
|
|
2753
|
+
}
|
|
2754
|
+
if (char === ")" || char === "}" || char === "]") {
|
|
2755
|
+
currentDepth = Math.max(0, currentDepth - 1);
|
|
2756
|
+
}
|
|
2757
|
+
}
|
|
2758
|
+
}
|
|
2759
|
+
if (maxFoundDepth > maxDepth) {
|
|
2760
|
+
violations.push({
|
|
2761
|
+
rule: "max-nesting-depth",
|
|
2762
|
+
message: `Schema nesting depth ${maxFoundDepth} exceeds maximum of ${maxDepth}. Consider breaking into smaller schemas.`,
|
|
2763
|
+
filePath,
|
|
2764
|
+
lineNumber: deepestLine,
|
|
2765
|
+
schemaName: "",
|
|
2766
|
+
severity: "warning",
|
|
2767
|
+
fixable: false
|
|
2768
|
+
});
|
|
2769
|
+
}
|
|
2770
|
+
return violations;
|
|
2771
|
+
}
|
|
2772
|
+
},
|
|
2773
|
+
{
|
|
2774
|
+
name: "no-deprecated-methods",
|
|
2775
|
+
description: "Flag usage of deprecated schema methods",
|
|
2776
|
+
category: "quality",
|
|
2777
|
+
rule: (sourceFile, _config) => {
|
|
2778
|
+
const violations = [];
|
|
2779
|
+
const text = sourceFile.getFullText();
|
|
2780
|
+
const filePath = sourceFile.getFilePath();
|
|
2781
|
+
const lines = text.split("\n");
|
|
2782
|
+
const deprecatedPatterns = [
|
|
2783
|
+
{
|
|
2784
|
+
pattern: /\.deepPartial\(\)/,
|
|
2785
|
+
message: ".deepPartial() is removed in Zod v4. Use recursive .partial() instead."
|
|
2786
|
+
},
|
|
2787
|
+
{
|
|
2788
|
+
pattern: /\.strip\(\)/,
|
|
2789
|
+
message: ".strip() is deprecated. Use z.strictObject() or explicit stripping."
|
|
2790
|
+
},
|
|
2791
|
+
{
|
|
2792
|
+
pattern: /z\.promise\(/,
|
|
2793
|
+
message: "z.promise() is deprecated in Zod v4. Use native Promise types."
|
|
2794
|
+
},
|
|
2795
|
+
{
|
|
2796
|
+
pattern: /z\.ostring\(\)/,
|
|
2797
|
+
message: "z.ostring() is removed in Zod v4. Use z.string().optional()."
|
|
2798
|
+
},
|
|
2799
|
+
{
|
|
2800
|
+
pattern: /z\.onumber\(\)/,
|
|
2801
|
+
message: "z.onumber() is removed in Zod v4. Use z.number().optional()."
|
|
2802
|
+
},
|
|
2803
|
+
{
|
|
2804
|
+
pattern: /z\.oboolean\(\)/,
|
|
2805
|
+
message: "z.oboolean() is removed in Zod v4. Use z.boolean().optional()."
|
|
2806
|
+
},
|
|
2807
|
+
{
|
|
2808
|
+
pattern: /z\.preprocess\(/,
|
|
2809
|
+
message: "z.preprocess() is removed in Zod v4. Use z.coerce.* instead."
|
|
2810
|
+
}
|
|
2811
|
+
];
|
|
2812
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2813
|
+
const line = lines[i] ?? "";
|
|
2814
|
+
for (const { pattern, message } of deprecatedPatterns) {
|
|
2815
|
+
if (pattern.test(line)) {
|
|
2816
|
+
violations.push({
|
|
2817
|
+
rule: "no-deprecated-methods",
|
|
2818
|
+
message,
|
|
2819
|
+
filePath,
|
|
2820
|
+
lineNumber: i + 1,
|
|
2821
|
+
schemaName: "",
|
|
2822
|
+
severity: "warning",
|
|
2823
|
+
fixable: false
|
|
2824
|
+
});
|
|
2825
|
+
}
|
|
2826
|
+
}
|
|
2827
|
+
}
|
|
2828
|
+
return violations;
|
|
2829
|
+
}
|
|
2830
|
+
},
|
|
2831
|
+
{
|
|
2832
|
+
name: "naming-convention",
|
|
2833
|
+
description: "Enforce schema naming pattern (e.g., must end with Schema)",
|
|
2834
|
+
category: "quality",
|
|
2835
|
+
rule: (sourceFile, config) => {
|
|
2836
|
+
const violations = [];
|
|
2837
|
+
const text = sourceFile.getFullText();
|
|
2838
|
+
const filePath = sourceFile.getFilePath();
|
|
2839
|
+
const lines = text.split("\n");
|
|
2840
|
+
const pattern = new RegExp(config.pattern || ".*Schema$");
|
|
2841
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2842
|
+
const line = lines[i] ?? "";
|
|
2843
|
+
const match = line.match(
|
|
2844
|
+
/(?:const|let)\s+(\w+)\s*=\s*(?:z\.|yup\.|Joi\.|t\.|v\.|type\(|object\(|string\()/
|
|
2845
|
+
);
|
|
2846
|
+
if (match?.[1] && !pattern.test(match[1])) {
|
|
2847
|
+
violations.push({
|
|
2848
|
+
rule: "naming-convention",
|
|
2849
|
+
message: `Schema "${match[1]}" does not match naming pattern ${pattern.source}.`,
|
|
2850
|
+
filePath,
|
|
2851
|
+
lineNumber: i + 1,
|
|
2852
|
+
schemaName: match[1],
|
|
2853
|
+
severity: "warning",
|
|
2854
|
+
fixable: false
|
|
2855
|
+
});
|
|
2856
|
+
}
|
|
2857
|
+
}
|
|
2858
|
+
return violations;
|
|
2859
|
+
}
|
|
2860
|
+
},
|
|
2861
|
+
{
|
|
2862
|
+
name: "require-max-length",
|
|
2863
|
+
description: "String schemas must have .max() to prevent DoS via unbounded input",
|
|
2864
|
+
category: "security",
|
|
2865
|
+
rule: (sourceFile, _config) => {
|
|
2866
|
+
const violations = [];
|
|
2867
|
+
const text = sourceFile.getFullText();
|
|
2868
|
+
const filePath = sourceFile.getFilePath();
|
|
2869
|
+
const lines = text.split("\n");
|
|
2870
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2871
|
+
const line = lines[i] ?? "";
|
|
2872
|
+
if (/z\.string\(\)/.test(line) && !line.includes(".max(") && !line.includes(".length(")) {
|
|
2873
|
+
let fullChain = line;
|
|
2874
|
+
let j = i + 1;
|
|
2875
|
+
while (j < lines.length && j < i + 5 && /^\s*\./.test(lines[j] ?? "")) {
|
|
2876
|
+
fullChain += lines[j] ?? "";
|
|
2877
|
+
j++;
|
|
2878
|
+
}
|
|
2879
|
+
if (!fullChain.includes(".max(") && !fullChain.includes(".length(")) {
|
|
2880
|
+
violations.push({
|
|
2881
|
+
rule: "require-max-length",
|
|
2882
|
+
message: "String schema should have .max() to prevent unbounded input (DoS protection).",
|
|
2883
|
+
filePath,
|
|
2884
|
+
lineNumber: i + 1,
|
|
2885
|
+
schemaName: "",
|
|
2886
|
+
severity: "warning",
|
|
2887
|
+
fixable: true
|
|
2888
|
+
});
|
|
2889
|
+
}
|
|
2890
|
+
}
|
|
2891
|
+
}
|
|
2892
|
+
return violations;
|
|
2893
|
+
}
|
|
2894
|
+
}
|
|
2895
|
+
];
|
|
2896
|
+
function getGovernanceTemplate(name) {
|
|
2897
|
+
return GOVERNANCE_TEMPLATES.find((t) => t.name === name);
|
|
2898
|
+
}
|
|
2899
|
+
function getGovernanceTemplatesByCategory(category) {
|
|
2900
|
+
return GOVERNANCE_TEMPLATES.filter((t) => t.category === category);
|
|
2901
|
+
}
|
|
2902
|
+
function getGovernanceTemplateNames() {
|
|
2903
|
+
return GOVERNANCE_TEMPLATES.map((t) => t.name);
|
|
2904
|
+
}
|
|
2905
|
+
|
|
2316
2906
|
// src/incremental.ts
|
|
2317
|
-
var
|
|
2318
|
-
var
|
|
2907
|
+
var import_node_fs7 = require("fs");
|
|
2908
|
+
var import_node_path7 = require("path");
|
|
2319
2909
|
var STATE_DIR = ".schemashift";
|
|
2320
2910
|
var STATE_FILE = "incremental.json";
|
|
2321
2911
|
var IncrementalTracker = class {
|
|
2322
2912
|
stateDir;
|
|
2323
2913
|
statePath;
|
|
2324
2914
|
constructor(projectPath) {
|
|
2325
|
-
this.stateDir = (0,
|
|
2326
|
-
this.statePath = (0,
|
|
2915
|
+
this.stateDir = (0, import_node_path7.join)(projectPath, STATE_DIR);
|
|
2916
|
+
this.statePath = (0, import_node_path7.join)(this.stateDir, STATE_FILE);
|
|
2327
2917
|
}
|
|
2328
2918
|
start(files, from, to) {
|
|
2329
2919
|
const state = {
|
|
@@ -2358,9 +2948,9 @@ var IncrementalTracker = class {
|
|
|
2358
2948
|
this.saveState(state);
|
|
2359
2949
|
}
|
|
2360
2950
|
getState() {
|
|
2361
|
-
if (!(0,
|
|
2951
|
+
if (!(0, import_node_fs7.existsSync)(this.statePath)) return null;
|
|
2362
2952
|
try {
|
|
2363
|
-
return JSON.parse((0,
|
|
2953
|
+
return JSON.parse((0, import_node_fs7.readFileSync)(this.statePath, "utf-8"));
|
|
2364
2954
|
} catch {
|
|
2365
2955
|
return null;
|
|
2366
2956
|
}
|
|
@@ -2387,21 +2977,21 @@ var IncrementalTracker = class {
|
|
|
2387
2977
|
};
|
|
2388
2978
|
}
|
|
2389
2979
|
clear() {
|
|
2390
|
-
if ((0,
|
|
2391
|
-
(0,
|
|
2980
|
+
if ((0, import_node_fs7.existsSync)(this.statePath)) {
|
|
2981
|
+
(0, import_node_fs7.unlinkSync)(this.statePath);
|
|
2392
2982
|
}
|
|
2393
2983
|
}
|
|
2394
2984
|
saveState(state) {
|
|
2395
|
-
if (!(0,
|
|
2396
|
-
(0,
|
|
2985
|
+
if (!(0, import_node_fs7.existsSync)(this.stateDir)) {
|
|
2986
|
+
(0, import_node_fs7.mkdirSync)(this.stateDir, { recursive: true });
|
|
2397
2987
|
}
|
|
2398
|
-
(0,
|
|
2988
|
+
(0, import_node_fs7.writeFileSync)(this.statePath, JSON.stringify(state, null, 2));
|
|
2399
2989
|
}
|
|
2400
2990
|
};
|
|
2401
2991
|
|
|
2402
2992
|
// src/package-updater.ts
|
|
2403
|
-
var
|
|
2404
|
-
var
|
|
2993
|
+
var import_node_fs8 = require("fs");
|
|
2994
|
+
var import_node_path8 = require("path");
|
|
2405
2995
|
var TARGET_VERSIONS = {
|
|
2406
2996
|
"yup->zod": { zod: "^3.24.0" },
|
|
2407
2997
|
"joi->zod": { zod: "^3.24.0" },
|
|
@@ -2422,14 +3012,14 @@ var PackageUpdater = class {
|
|
|
2422
3012
|
const add = {};
|
|
2423
3013
|
const remove = [];
|
|
2424
3014
|
const warnings = [];
|
|
2425
|
-
const pkgPath = (0,
|
|
2426
|
-
if (!(0,
|
|
3015
|
+
const pkgPath = (0, import_node_path8.join)(projectPath, "package.json");
|
|
3016
|
+
if (!(0, import_node_fs8.existsSync)(pkgPath)) {
|
|
2427
3017
|
warnings.push("No package.json found. Cannot plan dependency updates.");
|
|
2428
3018
|
return { add, remove, warnings };
|
|
2429
3019
|
}
|
|
2430
3020
|
let pkg;
|
|
2431
3021
|
try {
|
|
2432
|
-
pkg = JSON.parse((0,
|
|
3022
|
+
pkg = JSON.parse((0, import_node_fs8.readFileSync)(pkgPath, "utf-8"));
|
|
2433
3023
|
} catch {
|
|
2434
3024
|
warnings.push("Could not parse package.json.");
|
|
2435
3025
|
return { add, remove, warnings };
|
|
@@ -2459,9 +3049,9 @@ var PackageUpdater = class {
|
|
|
2459
3049
|
return { add, remove, warnings };
|
|
2460
3050
|
}
|
|
2461
3051
|
apply(projectPath, plan) {
|
|
2462
|
-
const pkgPath = (0,
|
|
2463
|
-
if (!(0,
|
|
2464
|
-
const pkgText = (0,
|
|
3052
|
+
const pkgPath = (0, import_node_path8.join)(projectPath, "package.json");
|
|
3053
|
+
if (!(0, import_node_fs8.existsSync)(pkgPath)) return;
|
|
3054
|
+
const pkgText = (0, import_node_fs8.readFileSync)(pkgPath, "utf-8");
|
|
2465
3055
|
const pkg = JSON.parse(pkgText);
|
|
2466
3056
|
if (!pkg.dependencies) pkg.dependencies = {};
|
|
2467
3057
|
for (const [name, version] of Object.entries(plan.add)) {
|
|
@@ -2471,7 +3061,7 @@ var PackageUpdater = class {
|
|
|
2471
3061
|
pkg.dependencies[name] = version;
|
|
2472
3062
|
}
|
|
2473
3063
|
}
|
|
2474
|
-
(0,
|
|
3064
|
+
(0, import_node_fs8.writeFileSync)(pkgPath, `${JSON.stringify(pkg, null, 2)}
|
|
2475
3065
|
`);
|
|
2476
3066
|
}
|
|
2477
3067
|
};
|
|
@@ -2643,8 +3233,8 @@ var PluginLoader = class {
|
|
|
2643
3233
|
};
|
|
2644
3234
|
|
|
2645
3235
|
// src/standard-schema.ts
|
|
2646
|
-
var
|
|
2647
|
-
var
|
|
3236
|
+
var import_node_fs9 = require("fs");
|
|
3237
|
+
var import_node_path9 = require("path");
|
|
2648
3238
|
var STANDARD_SCHEMA_LIBRARIES = {
|
|
2649
3239
|
zod: { minMajor: 3, minMinor: 23 },
|
|
2650
3240
|
// Zod v3.23+ and v4+
|
|
@@ -2673,13 +3263,13 @@ function isVersionCompatible(version, minMajor, minMinor) {
|
|
|
2673
3263
|
return false;
|
|
2674
3264
|
}
|
|
2675
3265
|
function detectStandardSchema(projectPath) {
|
|
2676
|
-
const pkgPath = (0,
|
|
2677
|
-
if (!(0,
|
|
3266
|
+
const pkgPath = (0, import_node_path9.join)(projectPath, "package.json");
|
|
3267
|
+
if (!(0, import_node_fs9.existsSync)(pkgPath)) {
|
|
2678
3268
|
return { detected: false, compatibleLibraries: [], recommendation: "", interopTools: [] };
|
|
2679
3269
|
}
|
|
2680
3270
|
let allDeps = {};
|
|
2681
3271
|
try {
|
|
2682
|
-
const pkg = JSON.parse((0,
|
|
3272
|
+
const pkg = JSON.parse((0, import_node_fs9.readFileSync)(pkgPath, "utf-8"));
|
|
2683
3273
|
allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
2684
3274
|
} catch {
|
|
2685
3275
|
return { detected: false, compatibleLibraries: [], recommendation: "", interopTools: [] };
|
|
@@ -3017,8 +3607,10 @@ var TypeDedupDetector = class {
|
|
|
3017
3607
|
CompatibilityAnalyzer,
|
|
3018
3608
|
ComplexityEstimator,
|
|
3019
3609
|
DetailedAnalyzer,
|
|
3610
|
+
DriftDetector,
|
|
3020
3611
|
EcosystemAnalyzer,
|
|
3021
3612
|
FormResolverMigrator,
|
|
3613
|
+
GOVERNANCE_TEMPLATES,
|
|
3022
3614
|
GovernanceEngine,
|
|
3023
3615
|
IncrementalTracker,
|
|
3024
3616
|
MigrationAuditLog,
|
|
@@ -3037,6 +3629,9 @@ var TypeDedupDetector = class {
|
|
|
3037
3629
|
detectFormLibraries,
|
|
3038
3630
|
detectSchemaLibrary,
|
|
3039
3631
|
detectStandardSchema,
|
|
3632
|
+
getGovernanceTemplate,
|
|
3633
|
+
getGovernanceTemplateNames,
|
|
3634
|
+
getGovernanceTemplatesByCategory,
|
|
3040
3635
|
isInsideComment,
|
|
3041
3636
|
isInsideStringLiteral,
|
|
3042
3637
|
loadConfig,
|