@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.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
|
|
2260
|
-
import { join as
|
|
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 =
|
|
2268
|
-
this.statePath =
|
|
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 (!
|
|
2888
|
+
if (!existsSync7(this.statePath)) return null;
|
|
2304
2889
|
try {
|
|
2305
|
-
return JSON.parse(
|
|
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 (
|
|
2917
|
+
if (existsSync7(this.statePath)) {
|
|
2333
2918
|
unlinkSync(this.statePath);
|
|
2334
2919
|
}
|
|
2335
2920
|
}
|
|
2336
2921
|
saveState(state) {
|
|
2337
|
-
if (!
|
|
2338
|
-
|
|
2922
|
+
if (!existsSync7(this.stateDir)) {
|
|
2923
|
+
mkdirSync3(this.stateDir, { recursive: true });
|
|
2339
2924
|
}
|
|
2340
|
-
|
|
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
|
|
2346
|
-
import { join as
|
|
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 =
|
|
2368
|
-
if (!
|
|
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(
|
|
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 =
|
|
2405
|
-
if (!
|
|
2406
|
-
const pkgText =
|
|
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
|
-
|
|
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
|
|
2589
|
-
import { join as
|
|
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 =
|
|
2619
|
-
if (!
|
|
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(
|
|
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,
|