@schemashift/core 0.11.0 → 0.13.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 +633 -32
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +156 -1
- package/dist/index.d.ts +156 -1
- package/dist/index.js +618 -32
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -136,10 +136,30 @@ var SchemaAnalyzer = class {
|
|
|
136
136
|
// src/approval.ts
|
|
137
137
|
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "fs";
|
|
138
138
|
import { join } from "path";
|
|
139
|
+
|
|
140
|
+
// src/constants.ts
|
|
141
|
+
var SCHEMASHIFT_DIR = ".schemashift";
|
|
142
|
+
var BACKUP_DIR = ".schemashift-backup";
|
|
143
|
+
var CONFIG_FILE_NAMES = [
|
|
144
|
+
".schemashiftrc",
|
|
145
|
+
".schemashiftrc.json",
|
|
146
|
+
".schemashiftrc.yaml",
|
|
147
|
+
".schemashiftrc.yml",
|
|
148
|
+
".schemashiftrc.js",
|
|
149
|
+
".schemashiftrc.cjs"
|
|
150
|
+
];
|
|
151
|
+
var DEFAULT_CONFIG_FILE = ".schemashiftrc.json";
|
|
152
|
+
var INCREMENTAL_STATE_FILE = "incremental.json";
|
|
153
|
+
var AUDIT_LOG_FILE = "audit-log.json";
|
|
154
|
+
var SCHEMA_SNAPSHOT_FILE = "schema-snapshot.json";
|
|
155
|
+
var PENDING_DIR = "pending";
|
|
156
|
+
var TESTS_DIR = "tests";
|
|
157
|
+
|
|
158
|
+
// src/approval.ts
|
|
139
159
|
var ApprovalManager = class {
|
|
140
160
|
pendingDir;
|
|
141
161
|
constructor(projectPath) {
|
|
142
|
-
this.pendingDir = join(projectPath,
|
|
162
|
+
this.pendingDir = join(projectPath, SCHEMASHIFT_DIR, PENDING_DIR);
|
|
143
163
|
}
|
|
144
164
|
/**
|
|
145
165
|
* Create a new migration request for review.
|
|
@@ -188,8 +208,16 @@ var ApprovalManager = class {
|
|
|
188
208
|
if (!existsSync(filePath)) {
|
|
189
209
|
return null;
|
|
190
210
|
}
|
|
191
|
-
|
|
192
|
-
|
|
211
|
+
try {
|
|
212
|
+
const content = readFileSync(filePath, "utf-8");
|
|
213
|
+
const parsed = JSON.parse(content);
|
|
214
|
+
if (!this.isValidRequest(parsed)) {
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
return parsed;
|
|
218
|
+
} catch {
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
193
221
|
}
|
|
194
222
|
/**
|
|
195
223
|
* List all migration requests, optionally filtered by status.
|
|
@@ -201,10 +229,14 @@ var ApprovalManager = class {
|
|
|
201
229
|
const files = readdirSync(this.pendingDir).filter((f) => f.endsWith(".json"));
|
|
202
230
|
const requests = [];
|
|
203
231
|
for (const file of files) {
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
232
|
+
try {
|
|
233
|
+
const content = readFileSync(join(this.pendingDir, file), "utf-8");
|
|
234
|
+
const parsed = JSON.parse(content);
|
|
235
|
+
if (!this.isValidRequest(parsed)) continue;
|
|
236
|
+
if (!status || parsed.status === status) {
|
|
237
|
+
requests.push(parsed);
|
|
238
|
+
}
|
|
239
|
+
} catch {
|
|
208
240
|
}
|
|
209
241
|
}
|
|
210
242
|
return requests.sort(
|
|
@@ -230,6 +262,11 @@ var ApprovalManager = class {
|
|
|
230
262
|
const request = this.getRequest(requestId);
|
|
231
263
|
return request?.status === "approved";
|
|
232
264
|
}
|
|
265
|
+
isValidRequest(data) {
|
|
266
|
+
if (typeof data !== "object" || data === null) return false;
|
|
267
|
+
const obj = data;
|
|
268
|
+
return typeof obj.id === "string" && typeof obj.from === "string" && typeof obj.to === "string" && Array.isArray(obj.files) && typeof obj.requestedBy === "string" && typeof obj.status === "string" && ["pending", "approved", "rejected"].includes(obj.status);
|
|
269
|
+
}
|
|
233
270
|
ensureDir() {
|
|
234
271
|
if (!existsSync(this.pendingDir)) {
|
|
235
272
|
mkdirSync(this.pendingDir, { recursive: true });
|
|
@@ -375,15 +412,13 @@ function transformMethodChain(chain, newBase, factoryMapper, methodMapper) {
|
|
|
375
412
|
import { createHash } from "crypto";
|
|
376
413
|
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
377
414
|
import { join as join2 } from "path";
|
|
378
|
-
var AUDIT_DIR = ".schemashift";
|
|
379
|
-
var AUDIT_FILE = "audit-log.json";
|
|
380
415
|
var AUDIT_VERSION = 1;
|
|
381
416
|
var MigrationAuditLog = class {
|
|
382
417
|
logDir;
|
|
383
418
|
logPath;
|
|
384
419
|
constructor(projectPath) {
|
|
385
|
-
this.logDir = join2(projectPath,
|
|
386
|
-
this.logPath = join2(this.logDir,
|
|
420
|
+
this.logDir = join2(projectPath, SCHEMASHIFT_DIR);
|
|
421
|
+
this.logPath = join2(this.logDir, AUDIT_LOG_FILE);
|
|
387
422
|
}
|
|
388
423
|
/**
|
|
389
424
|
* Append a new entry to the audit log.
|
|
@@ -427,7 +462,11 @@ var MigrationAuditLog = class {
|
|
|
427
462
|
if (!content.trim()) {
|
|
428
463
|
return { version: AUDIT_VERSION, entries: [] };
|
|
429
464
|
}
|
|
430
|
-
|
|
465
|
+
const parsed = JSON.parse(content);
|
|
466
|
+
if (!this.isValidAuditLog(parsed)) {
|
|
467
|
+
return { version: AUDIT_VERSION, entries: [] };
|
|
468
|
+
}
|
|
469
|
+
return parsed;
|
|
431
470
|
} catch {
|
|
432
471
|
return { version: AUDIT_VERSION, entries: [] };
|
|
433
472
|
}
|
|
@@ -505,6 +544,88 @@ var MigrationAuditLog = class {
|
|
|
505
544
|
clear() {
|
|
506
545
|
this.write({ version: AUDIT_VERSION, entries: [] });
|
|
507
546
|
}
|
|
547
|
+
/**
|
|
548
|
+
* Export a compliance report in SOC2 or HIPAA format.
|
|
549
|
+
*/
|
|
550
|
+
exportComplianceReport(format) {
|
|
551
|
+
const log = this.read();
|
|
552
|
+
const summary = this.getSummary();
|
|
553
|
+
if (format === "soc2") {
|
|
554
|
+
return this.generateSoc2Report(log, summary);
|
|
555
|
+
}
|
|
556
|
+
return this.generateHipaaReport(log, summary);
|
|
557
|
+
}
|
|
558
|
+
generateSoc2Report(log, summary) {
|
|
559
|
+
const sections = [];
|
|
560
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
561
|
+
sections.push("# SOC2 Compliance Report \u2014 Schema Migration");
|
|
562
|
+
sections.push(`Generated: ${now}`);
|
|
563
|
+
sections.push("");
|
|
564
|
+
sections.push("## Change Control Summary");
|
|
565
|
+
sections.push(`- Total Migrations: ${summary.totalMigrations}`);
|
|
566
|
+
sections.push(`- Total Files Processed: ${summary.totalFiles}`);
|
|
567
|
+
sections.push(`- Successful: ${summary.successCount}`);
|
|
568
|
+
sections.push(`- Failed: ${summary.failureCount}`);
|
|
569
|
+
sections.push(`- Migration Paths: ${summary.migrationPaths.join(", ")}`);
|
|
570
|
+
sections.push("");
|
|
571
|
+
sections.push("## Change Control Entries");
|
|
572
|
+
for (const entry of log.entries) {
|
|
573
|
+
sections.push("");
|
|
574
|
+
sections.push(`### ${entry.filePath}`);
|
|
575
|
+
sections.push(`- Change ID: ${entry.migrationId}`);
|
|
576
|
+
sections.push(`- Timestamp: ${entry.timestamp}`);
|
|
577
|
+
sections.push(`- Action: ${entry.action}`);
|
|
578
|
+
sections.push(`- Migration: ${entry.from} \u2192 ${entry.to}`);
|
|
579
|
+
sections.push(`- Status: ${entry.success ? "Success" : "Failed"}`);
|
|
580
|
+
sections.push(`- Implementer: ${entry.user || "Unknown"}`);
|
|
581
|
+
sections.push(`- Before Hash: ${entry.beforeHash}`);
|
|
582
|
+
if (entry.afterHash) sections.push(`- After Hash: ${entry.afterHash}`);
|
|
583
|
+
sections.push(`- Warnings: ${entry.warningCount}`);
|
|
584
|
+
sections.push(`- Errors: ${entry.errorCount}`);
|
|
585
|
+
if (entry.riskScore !== void 0) sections.push(`- Risk Score: ${entry.riskScore}`);
|
|
586
|
+
if (entry.metadata?.ciProvider) sections.push(`- CI Provider: ${entry.metadata.ciProvider}`);
|
|
587
|
+
if (entry.metadata?.gitCommit) sections.push(`- Git Commit: ${entry.metadata.gitCommit}`);
|
|
588
|
+
if (entry.metadata?.gitBranch) sections.push(`- Git Branch: ${entry.metadata.gitBranch}`);
|
|
589
|
+
}
|
|
590
|
+
sections.push("");
|
|
591
|
+
sections.push("## Rollback Procedure");
|
|
592
|
+
sections.push("SchemaShift maintains automatic backups in `.schemashift/backups/`.");
|
|
593
|
+
sections.push("Use `schemashift rollback [backupId]` to restore files from any backup.");
|
|
594
|
+
sections.push("");
|
|
595
|
+
return sections.join("\n");
|
|
596
|
+
}
|
|
597
|
+
generateHipaaReport(log, summary) {
|
|
598
|
+
const sections = [];
|
|
599
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
600
|
+
sections.push("# HIPAA Compliance Audit Trail \u2014 Schema Migration");
|
|
601
|
+
sections.push(`Generated: ${now}`);
|
|
602
|
+
sections.push("");
|
|
603
|
+
sections.push("## Data Transformation Summary");
|
|
604
|
+
sections.push(`- Total Transformations: ${summary.totalFiles}`);
|
|
605
|
+
sections.push(`- Successful: ${summary.successCount}`);
|
|
606
|
+
sections.push(`- Failed: ${summary.failureCount}`);
|
|
607
|
+
sections.push("");
|
|
608
|
+
sections.push("## Integrity Verification");
|
|
609
|
+
for (const entry of log.entries) {
|
|
610
|
+
sections.push("");
|
|
611
|
+
sections.push(`### ${entry.filePath}`);
|
|
612
|
+
sections.push(`- Timestamp: ${entry.timestamp}`);
|
|
613
|
+
sections.push(`- User: ${entry.user || "Unknown"}`);
|
|
614
|
+
sections.push(`- Action: ${entry.action} (${entry.from} \u2192 ${entry.to})`);
|
|
615
|
+
sections.push(`- Integrity Before: SHA256:${entry.beforeHash}`);
|
|
616
|
+
if (entry.afterHash) sections.push(`- Integrity After: SHA256:${entry.afterHash}`);
|
|
617
|
+
sections.push(`- Status: ${entry.success ? "Completed" : "Failed"}`);
|
|
618
|
+
if (entry.metadata?.hostname) sections.push(`- Host: ${entry.metadata.hostname}`);
|
|
619
|
+
if (entry.metadata?.nodeVersion)
|
|
620
|
+
sections.push(`- Runtime: Node.js ${entry.metadata.nodeVersion}`);
|
|
621
|
+
}
|
|
622
|
+
sections.push("");
|
|
623
|
+
sections.push("## Access Control");
|
|
624
|
+
const users = [...new Set(log.entries.map((e) => e.user).filter(Boolean))];
|
|
625
|
+
sections.push(`- Users Who Performed Migrations: ${users.join(", ") || "Unknown"}`);
|
|
626
|
+
sections.push("");
|
|
627
|
+
return sections.join("\n");
|
|
628
|
+
}
|
|
508
629
|
collectMetadata() {
|
|
509
630
|
return {
|
|
510
631
|
hostname: process.env.HOSTNAME || void 0,
|
|
@@ -515,6 +636,13 @@ var MigrationAuditLog = class {
|
|
|
515
636
|
gitCommit: process.env.GITHUB_SHA || process.env.CI_COMMIT_SHA || void 0
|
|
516
637
|
};
|
|
517
638
|
}
|
|
639
|
+
isValidAuditLog(data) {
|
|
640
|
+
if (typeof data !== "object" || data === null) return false;
|
|
641
|
+
const obj = data;
|
|
642
|
+
if (typeof obj.version !== "number") return false;
|
|
643
|
+
if (!Array.isArray(obj.entries)) return false;
|
|
644
|
+
return true;
|
|
645
|
+
}
|
|
518
646
|
write(log) {
|
|
519
647
|
if (!existsSync2(this.logDir)) {
|
|
520
648
|
mkdirSync2(this.logDir, { recursive: true });
|
|
@@ -958,6 +1086,12 @@ import { join as join4 } from "path";
|
|
|
958
1086
|
// src/ecosystem.ts
|
|
959
1087
|
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
|
|
960
1088
|
import { join as join3 } from "path";
|
|
1089
|
+
function parseMajorVersion(version) {
|
|
1090
|
+
const match = version.match(/(\d+)/);
|
|
1091
|
+
const num = match?.[1] ? Number.parseInt(match[1], 10) : 0;
|
|
1092
|
+
if (!Number.isFinite(num) || num < 0 || num > 999) return 0;
|
|
1093
|
+
return num;
|
|
1094
|
+
}
|
|
961
1095
|
var ECOSYSTEM_RULES = [
|
|
962
1096
|
// ORM integrations
|
|
963
1097
|
{
|
|
@@ -999,8 +1133,7 @@ var ECOSYSTEM_RULES = [
|
|
|
999
1133
|
category: "api",
|
|
1000
1134
|
migrations: ["zod-v3->v4"],
|
|
1001
1135
|
check: (version) => {
|
|
1002
|
-
const
|
|
1003
|
-
const major = majorMatch?.[1] ? Number.parseInt(majorMatch[1], 10) : 0;
|
|
1136
|
+
const major = parseMajorVersion(version);
|
|
1004
1137
|
if (major < 11) {
|
|
1005
1138
|
return {
|
|
1006
1139
|
issue: `tRPC v${major} expects Zod v3 types. A v3 ZodType is not assignable to a v4 ZodType.`,
|
|
@@ -1033,8 +1166,7 @@ var ECOSYSTEM_RULES = [
|
|
|
1033
1166
|
category: "validation-util",
|
|
1034
1167
|
migrations: ["zod-v3->v4"],
|
|
1035
1168
|
check: (version) => {
|
|
1036
|
-
const
|
|
1037
|
-
const major = majorMatch?.[1] ? Number.parseInt(majorMatch[1], 10) : 0;
|
|
1169
|
+
const major = parseMajorVersion(version);
|
|
1038
1170
|
if (major < 4) {
|
|
1039
1171
|
return {
|
|
1040
1172
|
issue: `zod-validation-error v${major} is not compatible with Zod v4.`,
|
|
@@ -1324,8 +1456,7 @@ var ECOSYSTEM_RULES = [
|
|
|
1324
1456
|
category: "validation-util",
|
|
1325
1457
|
migrations: ["zod-v3->v4"],
|
|
1326
1458
|
check: (version) => {
|
|
1327
|
-
const
|
|
1328
|
-
const major = majorMatch?.[1] ? Number.parseInt(majorMatch[1], 10) : 0;
|
|
1459
|
+
const major = parseMajorVersion(version);
|
|
1329
1460
|
if (major < 4) {
|
|
1330
1461
|
return {
|
|
1331
1462
|
issue: "zod-to-json-schema v3 may not fully support Zod v4 schemas.",
|
|
@@ -1637,6 +1768,25 @@ var ComplexityEstimator = class {
|
|
|
1637
1768
|
riskAreas
|
|
1638
1769
|
};
|
|
1639
1770
|
}
|
|
1771
|
+
estimateDuration(estimate) {
|
|
1772
|
+
const EFFORT_RANGES = {
|
|
1773
|
+
trivial: { label: "1\u20135 minutes", range: [1, 5] },
|
|
1774
|
+
low: { label: "5\u201315 minutes", range: [5, 15] },
|
|
1775
|
+
moderate: { label: "15\u201345 minutes", range: [15, 45] },
|
|
1776
|
+
high: { label: "1\u20133 hours", range: [60, 180] },
|
|
1777
|
+
extreme: { label: "3\u20138 hours", range: [180, 480] }
|
|
1778
|
+
};
|
|
1779
|
+
const base = EFFORT_RANGES[estimate.effort];
|
|
1780
|
+
const fileMultiplier = Math.max(1, Math.log2(estimate.totalFiles + 1));
|
|
1781
|
+
const low = Math.round(base.range[0] * fileMultiplier);
|
|
1782
|
+
const high = Math.round(base.range[1] * fileMultiplier);
|
|
1783
|
+
if (high >= 120) {
|
|
1784
|
+
const lowHours = Math.round(low / 60 * 10) / 10;
|
|
1785
|
+
const highHours = Math.round(high / 60 * 10) / 10;
|
|
1786
|
+
return { label: `${lowHours}\u2013${highHours} hours`, rangeMinutes: [low, high] };
|
|
1787
|
+
}
|
|
1788
|
+
return { label: `${low}\u2013${high} minutes`, rangeMinutes: [low, high] };
|
|
1789
|
+
}
|
|
1640
1790
|
calculateEffort(totalSchemas, advancedCount, hasDeepDU) {
|
|
1641
1791
|
if (totalSchemas >= 500 && hasDeepDU) return "extreme";
|
|
1642
1792
|
if (totalSchemas >= 200 || advancedCount >= 20) return "high";
|
|
@@ -1701,7 +1851,7 @@ async function loadConfig(configPath) {
|
|
|
1701
1851
|
include: ["**/*.ts", "**/*.tsx"],
|
|
1702
1852
|
exclude: ["**/node_modules/**", "**/dist/**", "**/*.d.ts"],
|
|
1703
1853
|
git: { enabled: false },
|
|
1704
|
-
backup: { enabled: true, dir:
|
|
1854
|
+
backup: { enabled: true, dir: BACKUP_DIR },
|
|
1705
1855
|
...result?.config
|
|
1706
1856
|
};
|
|
1707
1857
|
}
|
|
@@ -1812,6 +1962,75 @@ function suggestCrossFieldPattern(whenCode) {
|
|
|
1812
1962
|
return null;
|
|
1813
1963
|
}
|
|
1814
1964
|
|
|
1965
|
+
// src/dead-schema-detector.ts
|
|
1966
|
+
var DeadSchemaDetector = class {
|
|
1967
|
+
detect(sourceFiles) {
|
|
1968
|
+
const schemas = this.collectSchemaDefinitions(sourceFiles);
|
|
1969
|
+
const unusedSchemas = this.findUnusedSchemas(schemas, sourceFiles);
|
|
1970
|
+
return {
|
|
1971
|
+
unusedSchemas,
|
|
1972
|
+
totalSchemas: schemas.length,
|
|
1973
|
+
summary: unusedSchemas.length > 0 ? `Found ${unusedSchemas.length} unused schema(s) out of ${schemas.length} total that may be safely removed.` : `All ${schemas.length} schema(s) are referenced.`
|
|
1974
|
+
};
|
|
1975
|
+
}
|
|
1976
|
+
collectSchemaDefinitions(sourceFiles) {
|
|
1977
|
+
const schemas = [];
|
|
1978
|
+
const schemaPattern = /(?:const|let|var|export\s+(?:const|let|var))\s+(\w+)\s*=\s*(?:z\.|yup\.|Yup\.|Joi\.|t\.|v\.|type\(|object\(|string\(|S\.)/;
|
|
1979
|
+
for (const file of sourceFiles) {
|
|
1980
|
+
const text = file.getFullText();
|
|
1981
|
+
const lines = text.split("\n");
|
|
1982
|
+
const filePath = file.getFilePath();
|
|
1983
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1984
|
+
const line = lines[i];
|
|
1985
|
+
if (!line) continue;
|
|
1986
|
+
const match = schemaPattern.exec(line);
|
|
1987
|
+
if (match?.[1]) {
|
|
1988
|
+
schemas.push({
|
|
1989
|
+
schemaName: match[1],
|
|
1990
|
+
filePath,
|
|
1991
|
+
lineNumber: i + 1
|
|
1992
|
+
});
|
|
1993
|
+
}
|
|
1994
|
+
}
|
|
1995
|
+
}
|
|
1996
|
+
return schemas;
|
|
1997
|
+
}
|
|
1998
|
+
findUnusedSchemas(schemas, sourceFiles) {
|
|
1999
|
+
const fileContents = /* @__PURE__ */ new Map();
|
|
2000
|
+
for (const file of sourceFiles) {
|
|
2001
|
+
fileContents.set(file.getFilePath(), file.getFullText());
|
|
2002
|
+
}
|
|
2003
|
+
const unused = [];
|
|
2004
|
+
for (const schema of schemas) {
|
|
2005
|
+
const { schemaName, filePath } = schema;
|
|
2006
|
+
let referenceCount = 0;
|
|
2007
|
+
for (const [path, content] of fileContents) {
|
|
2008
|
+
const pattern = new RegExp(`\\b${schemaName}\\b`, "g");
|
|
2009
|
+
const matches = content.match(pattern);
|
|
2010
|
+
const matchCount = matches?.length ?? 0;
|
|
2011
|
+
if (path === filePath) {
|
|
2012
|
+
if (matchCount > 1) {
|
|
2013
|
+
referenceCount += matchCount - 1;
|
|
2014
|
+
}
|
|
2015
|
+
} else {
|
|
2016
|
+
referenceCount += matchCount;
|
|
2017
|
+
}
|
|
2018
|
+
}
|
|
2019
|
+
const fileContent = fileContents.get(filePath) ?? "";
|
|
2020
|
+
const exportPattern = new RegExp(
|
|
2021
|
+
`export\\s+(?:const|let|var)\\s+${schemaName}\\b|export\\s*\\{[^}]*\\b${schemaName}\\b`
|
|
2022
|
+
);
|
|
2023
|
+
if (exportPattern.test(fileContent)) {
|
|
2024
|
+
referenceCount++;
|
|
2025
|
+
}
|
|
2026
|
+
if (referenceCount === 0) {
|
|
2027
|
+
unused.push(schema);
|
|
2028
|
+
}
|
|
2029
|
+
}
|
|
2030
|
+
return unused;
|
|
2031
|
+
}
|
|
2032
|
+
};
|
|
2033
|
+
|
|
1815
2034
|
// src/dependency-graph.ts
|
|
1816
2035
|
import { existsSync as existsSync5, readdirSync as readdirSync2, readFileSync as readFileSync5 } from "fs";
|
|
1817
2036
|
import { join as join5, resolve } from "path";
|
|
@@ -2352,15 +2571,13 @@ var DetailedAnalyzer = class {
|
|
|
2352
2571
|
import { createHash as createHash2 } from "crypto";
|
|
2353
2572
|
import { existsSync as existsSync7, mkdirSync as mkdirSync3, readFileSync as readFileSync7, writeFileSync as writeFileSync3 } from "fs";
|
|
2354
2573
|
import { join as join7, relative } from "path";
|
|
2355
|
-
var SNAPSHOT_DIR = ".schemashift";
|
|
2356
|
-
var SNAPSHOT_FILE = "schema-snapshot.json";
|
|
2357
2574
|
var SNAPSHOT_VERSION = 1;
|
|
2358
2575
|
var DriftDetector = class {
|
|
2359
2576
|
snapshotDir;
|
|
2360
2577
|
snapshotPath;
|
|
2361
2578
|
constructor(projectPath) {
|
|
2362
|
-
this.snapshotDir = join7(projectPath,
|
|
2363
|
-
this.snapshotPath = join7(this.snapshotDir,
|
|
2579
|
+
this.snapshotDir = join7(projectPath, SCHEMASHIFT_DIR);
|
|
2580
|
+
this.snapshotPath = join7(this.snapshotDir, SCHEMA_SNAPSHOT_FILE);
|
|
2364
2581
|
}
|
|
2365
2582
|
/**
|
|
2366
2583
|
* Take a snapshot of the current schema state
|
|
@@ -2630,7 +2847,8 @@ var GovernanceEngine = class {
|
|
|
2630
2847
|
if (this.rules.has("naming-convention")) {
|
|
2631
2848
|
const config = this.rules.get("naming-convention") ?? {};
|
|
2632
2849
|
const pattern = config.pattern || ".*Schema$";
|
|
2633
|
-
|
|
2850
|
+
const regex = this.safeRegExp(pattern);
|
|
2851
|
+
if (regex && !regex.test(schemaName)) {
|
|
2634
2852
|
violations.push({
|
|
2635
2853
|
rule: "naming-convention",
|
|
2636
2854
|
message: `Schema "${schemaName}" does not match naming pattern: ${pattern}`,
|
|
@@ -2792,6 +3010,14 @@ var GovernanceEngine = class {
|
|
|
2792
3010
|
passed: violations.filter((v) => v.severity === "error").length === 0
|
|
2793
3011
|
};
|
|
2794
3012
|
}
|
|
3013
|
+
safeRegExp(pattern) {
|
|
3014
|
+
if (pattern.length > 500) return null;
|
|
3015
|
+
try {
|
|
3016
|
+
return new RegExp(pattern);
|
|
3017
|
+
} catch {
|
|
3018
|
+
return null;
|
|
3019
|
+
}
|
|
3020
|
+
}
|
|
2795
3021
|
detectFileLibrary(sourceFile) {
|
|
2796
3022
|
for (const imp of sourceFile.getImportDeclarations()) {
|
|
2797
3023
|
const lib = detectSchemaLibrary(imp.getModuleSpecifierValue());
|
|
@@ -3527,17 +3753,77 @@ var GraphExporter = class {
|
|
|
3527
3753
|
}
|
|
3528
3754
|
};
|
|
3529
3755
|
|
|
3756
|
+
// src/import-deduplicator.ts
|
|
3757
|
+
var ImportDeduplicator = class {
|
|
3758
|
+
detect(sourceFiles) {
|
|
3759
|
+
const allGroups = [];
|
|
3760
|
+
for (const file of sourceFiles) {
|
|
3761
|
+
const groups = this.findDuplicatesInFile(file);
|
|
3762
|
+
allGroups.push(...groups);
|
|
3763
|
+
}
|
|
3764
|
+
const totalDuplicates = allGroups.reduce((sum, g) => sum + g.occurrences.length, 0);
|
|
3765
|
+
return {
|
|
3766
|
+
duplicateGroups: allGroups,
|
|
3767
|
+
totalDuplicates,
|
|
3768
|
+
summary: allGroups.length > 0 ? `Found ${allGroups.length} duplicate import group(s) across ${new Set(allGroups.map((g) => g.occurrences[0]?.filePath)).size} file(s). Merge them for cleaner imports.` : "No duplicate imports found."
|
|
3769
|
+
};
|
|
3770
|
+
}
|
|
3771
|
+
findDuplicatesInFile(sourceFile) {
|
|
3772
|
+
const imports = sourceFile.getImportDeclarations();
|
|
3773
|
+
const filePath = sourceFile.getFilePath();
|
|
3774
|
+
const bySource = /* @__PURE__ */ new Map();
|
|
3775
|
+
for (const imp of imports) {
|
|
3776
|
+
const source = imp.getModuleSpecifierValue();
|
|
3777
|
+
const namedImports = imp.getNamedImports().map((n) => n.getName());
|
|
3778
|
+
const namespaceImport = imp.getNamespaceImport()?.getText();
|
|
3779
|
+
const defaultImport = imp.getDefaultImport()?.getText();
|
|
3780
|
+
const importedNames = [];
|
|
3781
|
+
if (defaultImport) importedNames.push(defaultImport);
|
|
3782
|
+
if (namespaceImport) importedNames.push(`* as ${namespaceImport}`);
|
|
3783
|
+
importedNames.push(...namedImports);
|
|
3784
|
+
if (importedNames.length === 0) continue;
|
|
3785
|
+
const entry = {
|
|
3786
|
+
source,
|
|
3787
|
+
filePath,
|
|
3788
|
+
lineNumber: imp.getStartLineNumber(),
|
|
3789
|
+
importedNames
|
|
3790
|
+
};
|
|
3791
|
+
const existing = bySource.get(source);
|
|
3792
|
+
if (existing) {
|
|
3793
|
+
existing.push(entry);
|
|
3794
|
+
} else {
|
|
3795
|
+
bySource.set(source, [entry]);
|
|
3796
|
+
}
|
|
3797
|
+
}
|
|
3798
|
+
const groups = [];
|
|
3799
|
+
for (const [source, occurrences] of bySource) {
|
|
3800
|
+
if (occurrences.length <= 1) continue;
|
|
3801
|
+
const allNames = /* @__PURE__ */ new Set();
|
|
3802
|
+
for (const occ of occurrences) {
|
|
3803
|
+
for (const name of occ.importedNames) {
|
|
3804
|
+
allNames.add(name);
|
|
3805
|
+
}
|
|
3806
|
+
}
|
|
3807
|
+
const mergedNames = [...allNames].sort().join(", ");
|
|
3808
|
+
groups.push({
|
|
3809
|
+
source,
|
|
3810
|
+
occurrences,
|
|
3811
|
+
suggestion: `Merge into single import: import { ${mergedNames} } from '${source}';`
|
|
3812
|
+
});
|
|
3813
|
+
}
|
|
3814
|
+
return groups;
|
|
3815
|
+
}
|
|
3816
|
+
};
|
|
3817
|
+
|
|
3530
3818
|
// src/incremental.ts
|
|
3531
3819
|
import { existsSync as existsSync8, mkdirSync as mkdirSync4, readFileSync as readFileSync8, unlinkSync, writeFileSync as writeFileSync4 } from "fs";
|
|
3532
3820
|
import { join as join8 } from "path";
|
|
3533
|
-
var STATE_DIR = ".schemashift";
|
|
3534
|
-
var STATE_FILE = "incremental.json";
|
|
3535
3821
|
var IncrementalTracker = class {
|
|
3536
3822
|
stateDir;
|
|
3537
3823
|
statePath;
|
|
3538
3824
|
constructor(projectPath) {
|
|
3539
|
-
this.stateDir = join8(projectPath,
|
|
3540
|
-
this.statePath = join8(this.stateDir,
|
|
3825
|
+
this.stateDir = join8(projectPath, SCHEMASHIFT_DIR);
|
|
3826
|
+
this.statePath = join8(this.stateDir, INCREMENTAL_STATE_FILE);
|
|
3541
3827
|
}
|
|
3542
3828
|
start(files, from, to) {
|
|
3543
3829
|
const state = {
|
|
@@ -3574,7 +3860,9 @@ var IncrementalTracker = class {
|
|
|
3574
3860
|
getState() {
|
|
3575
3861
|
if (!existsSync8(this.statePath)) return null;
|
|
3576
3862
|
try {
|
|
3577
|
-
|
|
3863
|
+
const parsed = JSON.parse(readFileSync8(this.statePath, "utf-8"));
|
|
3864
|
+
if (!this.isValidState(parsed)) return null;
|
|
3865
|
+
return parsed;
|
|
3578
3866
|
} catch {
|
|
3579
3867
|
return null;
|
|
3580
3868
|
}
|
|
@@ -3600,11 +3888,32 @@ var IncrementalTracker = class {
|
|
|
3600
3888
|
percent
|
|
3601
3889
|
};
|
|
3602
3890
|
}
|
|
3891
|
+
/**
|
|
3892
|
+
* Get a canary batch — a percentage of remaining files, sorted simplest first.
|
|
3893
|
+
* Used for phased rollouts where you migrate a small batch, verify, then continue.
|
|
3894
|
+
*/
|
|
3895
|
+
getCanaryBatch(percent, fileSizes) {
|
|
3896
|
+
const state = this.getState();
|
|
3897
|
+
if (!state) return [];
|
|
3898
|
+
const count = Math.max(1, Math.ceil(state.remainingFiles.length * (percent / 100)));
|
|
3899
|
+
if (fileSizes) {
|
|
3900
|
+
const sorted = [...state.remainingFiles].sort((a, b) => {
|
|
3901
|
+
return (fileSizes.get(a) ?? 0) - (fileSizes.get(b) ?? 0);
|
|
3902
|
+
});
|
|
3903
|
+
return sorted.slice(0, count);
|
|
3904
|
+
}
|
|
3905
|
+
return state.remainingFiles.slice(0, count);
|
|
3906
|
+
}
|
|
3603
3907
|
clear() {
|
|
3604
3908
|
if (existsSync8(this.statePath)) {
|
|
3605
3909
|
unlinkSync(this.statePath);
|
|
3606
3910
|
}
|
|
3607
3911
|
}
|
|
3912
|
+
isValidState(data) {
|
|
3913
|
+
if (typeof data !== "object" || data === null) return false;
|
|
3914
|
+
const obj = data;
|
|
3915
|
+
return typeof obj.migrationId === "string" && typeof obj.from === "string" && typeof obj.to === "string" && typeof obj.startedAt === "string" && Array.isArray(obj.completedFiles) && Array.isArray(obj.remainingFiles) && Array.isArray(obj.failedFiles);
|
|
3916
|
+
}
|
|
3608
3917
|
saveState(state) {
|
|
3609
3918
|
if (!existsSync8(this.stateDir)) {
|
|
3610
3919
|
mkdirSync4(this.stateDir, { recursive: true });
|
|
@@ -3814,11 +4123,111 @@ var WebhookNotifier = class {
|
|
|
3814
4123
|
}
|
|
3815
4124
|
return results;
|
|
3816
4125
|
}
|
|
4126
|
+
/**
|
|
4127
|
+
* Format event as Slack Block Kit message.
|
|
4128
|
+
*/
|
|
4129
|
+
formatSlackPayload(event) {
|
|
4130
|
+
const emoji = this.getEventEmoji(event.type);
|
|
4131
|
+
const title = this.getEventTitle(event.type);
|
|
4132
|
+
const details = event.details;
|
|
4133
|
+
const blocks = [
|
|
4134
|
+
{
|
|
4135
|
+
type: "header",
|
|
4136
|
+
text: { type: "plain_text", text: `${emoji} ${title}`, emoji: true }
|
|
4137
|
+
},
|
|
4138
|
+
{
|
|
4139
|
+
type: "section",
|
|
4140
|
+
fields: Object.entries(details).map(([key, value]) => ({
|
|
4141
|
+
type: "mrkdwn",
|
|
4142
|
+
text: `*${key}:* ${String(value)}`
|
|
4143
|
+
}))
|
|
4144
|
+
},
|
|
4145
|
+
{
|
|
4146
|
+
type: "context",
|
|
4147
|
+
elements: [
|
|
4148
|
+
{
|
|
4149
|
+
type: "mrkdwn",
|
|
4150
|
+
text: `SchemaShift | ${event.timestamp}${event.project ? ` | ${event.project}` : ""}`
|
|
4151
|
+
}
|
|
4152
|
+
]
|
|
4153
|
+
}
|
|
4154
|
+
];
|
|
4155
|
+
return { blocks };
|
|
4156
|
+
}
|
|
4157
|
+
/**
|
|
4158
|
+
* Format event as Microsoft Teams Adaptive Card.
|
|
4159
|
+
*/
|
|
4160
|
+
formatTeamsPayload(event) {
|
|
4161
|
+
const title = this.getEventTitle(event.type);
|
|
4162
|
+
const details = event.details;
|
|
4163
|
+
const facts = Object.entries(details).map(([key, value]) => ({
|
|
4164
|
+
title: key,
|
|
4165
|
+
value: String(value)
|
|
4166
|
+
}));
|
|
4167
|
+
return {
|
|
4168
|
+
type: "message",
|
|
4169
|
+
attachments: [
|
|
4170
|
+
{
|
|
4171
|
+
contentType: "application/vnd.microsoft.card.adaptive",
|
|
4172
|
+
content: {
|
|
4173
|
+
$schema: "http://adaptivecards.io/schemas/adaptive-card.json",
|
|
4174
|
+
type: "AdaptiveCard",
|
|
4175
|
+
version: "1.4",
|
|
4176
|
+
body: [
|
|
4177
|
+
{
|
|
4178
|
+
type: "TextBlock",
|
|
4179
|
+
text: title,
|
|
4180
|
+
weight: "Bolder",
|
|
4181
|
+
size: "Medium"
|
|
4182
|
+
},
|
|
4183
|
+
{
|
|
4184
|
+
type: "FactSet",
|
|
4185
|
+
facts
|
|
4186
|
+
},
|
|
4187
|
+
{
|
|
4188
|
+
type: "TextBlock",
|
|
4189
|
+
text: `SchemaShift | ${event.timestamp}`,
|
|
4190
|
+
isSubtle: true,
|
|
4191
|
+
size: "Small"
|
|
4192
|
+
}
|
|
4193
|
+
]
|
|
4194
|
+
}
|
|
4195
|
+
}
|
|
4196
|
+
]
|
|
4197
|
+
};
|
|
4198
|
+
}
|
|
4199
|
+
getEventEmoji(type) {
|
|
4200
|
+
const emojis = {
|
|
4201
|
+
migration_started: "\u{1F504}",
|
|
4202
|
+
migration_completed: "\u2705",
|
|
4203
|
+
migration_failed: "\u274C",
|
|
4204
|
+
governance_violation: "\u26A0\uFE0F",
|
|
4205
|
+
drift_detected: "\u{1F50D}"
|
|
4206
|
+
};
|
|
4207
|
+
return emojis[type];
|
|
4208
|
+
}
|
|
4209
|
+
getEventTitle(type) {
|
|
4210
|
+
const titles = {
|
|
4211
|
+
migration_started: "Migration Started",
|
|
4212
|
+
migration_completed: "Migration Completed",
|
|
4213
|
+
migration_failed: "Migration Failed",
|
|
4214
|
+
governance_violation: "Governance Violation",
|
|
4215
|
+
drift_detected: "Schema Drift Detected"
|
|
4216
|
+
};
|
|
4217
|
+
return titles[type];
|
|
4218
|
+
}
|
|
3817
4219
|
/**
|
|
3818
4220
|
* Send event to a single webhook endpoint.
|
|
3819
4221
|
*/
|
|
3820
4222
|
async sendToWebhook(webhook, event) {
|
|
3821
|
-
|
|
4223
|
+
let payload;
|
|
4224
|
+
if (webhook.type === "slack") {
|
|
4225
|
+
payload = JSON.stringify(this.formatSlackPayload(event));
|
|
4226
|
+
} else if (webhook.type === "teams") {
|
|
4227
|
+
payload = JSON.stringify(this.formatTeamsPayload(event));
|
|
4228
|
+
} else {
|
|
4229
|
+
payload = JSON.stringify(event);
|
|
4230
|
+
}
|
|
3822
4231
|
const headers = {
|
|
3823
4232
|
"Content-Type": "application/json",
|
|
3824
4233
|
"User-Agent": "SchemaShift-Webhook/1.0",
|
|
@@ -3828,11 +4237,15 @@ var WebhookNotifier = class {
|
|
|
3828
4237
|
const signature = await computeSignature(payload, webhook.secret);
|
|
3829
4238
|
headers["X-SchemaShift-Signature"] = `sha256=${signature}`;
|
|
3830
4239
|
}
|
|
4240
|
+
const timeoutMs = webhook.timeoutMs ?? 1e4;
|
|
4241
|
+
const controller = new AbortController();
|
|
4242
|
+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
3831
4243
|
try {
|
|
3832
4244
|
const response = await fetch(webhook.url, {
|
|
3833
4245
|
method: "POST",
|
|
3834
4246
|
headers,
|
|
3835
|
-
body: payload
|
|
4247
|
+
body: payload,
|
|
4248
|
+
signal: controller.signal
|
|
3836
4249
|
});
|
|
3837
4250
|
return {
|
|
3838
4251
|
success: response.ok,
|
|
@@ -3840,10 +4253,13 @@ var WebhookNotifier = class {
|
|
|
3840
4253
|
error: response.ok ? void 0 : `HTTP ${response.status}: ${response.statusText}`
|
|
3841
4254
|
};
|
|
3842
4255
|
} catch (err) {
|
|
4256
|
+
const message = err instanceof Error && err.name === "AbortError" ? `Webhook request timed out after ${timeoutMs}ms` : err instanceof Error ? err.message : String(err);
|
|
3843
4257
|
return {
|
|
3844
4258
|
success: false,
|
|
3845
|
-
error:
|
|
4259
|
+
error: message
|
|
3846
4260
|
};
|
|
4261
|
+
} finally {
|
|
4262
|
+
clearTimeout(timeoutId);
|
|
3847
4263
|
}
|
|
3848
4264
|
}
|
|
3849
4265
|
/**
|
|
@@ -4134,6 +4550,161 @@ var PluginLoader = class {
|
|
|
4134
4550
|
}
|
|
4135
4551
|
};
|
|
4136
4552
|
|
|
4553
|
+
// src/schema-verifier.ts
|
|
4554
|
+
var PRIMITIVE_SAMPLES = {
|
|
4555
|
+
string: [
|
|
4556
|
+
{ name: "empty string", input: "", expectedValid: true },
|
|
4557
|
+
{ name: "normal string", input: "hello world", expectedValid: true },
|
|
4558
|
+
{ name: "number as string", input: "12345", expectedValid: true },
|
|
4559
|
+
{ name: "null input", input: null, expectedValid: false },
|
|
4560
|
+
{ name: "number input", input: 42, expectedValid: false },
|
|
4561
|
+
{ name: "boolean input", input: true, expectedValid: false },
|
|
4562
|
+
{ name: "undefined input", input: void 0, expectedValid: false }
|
|
4563
|
+
],
|
|
4564
|
+
number: [
|
|
4565
|
+
{ name: "zero", input: 0, expectedValid: true },
|
|
4566
|
+
{ name: "positive int", input: 42, expectedValid: true },
|
|
4567
|
+
{ name: "negative int", input: -1, expectedValid: true },
|
|
4568
|
+
{ name: "float", input: 3.14, expectedValid: true },
|
|
4569
|
+
{ name: "string input", input: "hello", expectedValid: false },
|
|
4570
|
+
{ name: "null input", input: null, expectedValid: false },
|
|
4571
|
+
{ name: "NaN input", input: Number.NaN, expectedValid: false }
|
|
4572
|
+
],
|
|
4573
|
+
boolean: [
|
|
4574
|
+
{ name: "true", input: true, expectedValid: true },
|
|
4575
|
+
{ name: "false", input: false, expectedValid: true },
|
|
4576
|
+
{ name: "string input", input: "true", expectedValid: false },
|
|
4577
|
+
{ name: "number input", input: 1, expectedValid: false },
|
|
4578
|
+
{ name: "null input", input: null, expectedValid: false }
|
|
4579
|
+
],
|
|
4580
|
+
date: [
|
|
4581
|
+
{ name: "valid date", input: /* @__PURE__ */ new Date("2024-01-01"), expectedValid: true },
|
|
4582
|
+
{ name: "string input", input: "2024-01-01", expectedValid: false },
|
|
4583
|
+
{ name: "null input", input: null, expectedValid: false }
|
|
4584
|
+
]
|
|
4585
|
+
};
|
|
4586
|
+
var EMAIL_SAMPLES = [
|
|
4587
|
+
{ name: "valid email", input: "test@example.com", expectedValid: true },
|
|
4588
|
+
{ name: "invalid email", input: "not-an-email", expectedValid: false },
|
|
4589
|
+
{ name: "empty string", input: "", expectedValid: false }
|
|
4590
|
+
];
|
|
4591
|
+
var URL_SAMPLES = [
|
|
4592
|
+
{ name: "valid url", input: "https://example.com", expectedValid: true },
|
|
4593
|
+
{ name: "invalid url", input: "not a url", expectedValid: false }
|
|
4594
|
+
];
|
|
4595
|
+
var UUID_SAMPLES = [
|
|
4596
|
+
{ name: "valid uuid", input: "550e8400-e29b-41d4-a716-446655440000", expectedValid: true },
|
|
4597
|
+
{ name: "invalid uuid", input: "not-a-uuid", expectedValid: false }
|
|
4598
|
+
];
|
|
4599
|
+
function extractSchemaNames(sourceText) {
|
|
4600
|
+
const schemas = [];
|
|
4601
|
+
const patterns = [
|
|
4602
|
+
/(?:const|let|var)\s+(\w+)\s*=\s*(?:z\.|yup\.|Joi\.|v\.|t\.|S\.|type\(|object\(|string\()/g,
|
|
4603
|
+
/export\s+(?:const|let|var)\s+(\w+)\s*=\s*(?:z\.|yup\.|Joi\.|v\.|t\.|S\.|type\(|object\(|string\()/g
|
|
4604
|
+
];
|
|
4605
|
+
for (const pattern of patterns) {
|
|
4606
|
+
for (const match of sourceText.matchAll(pattern)) {
|
|
4607
|
+
const name = match[1];
|
|
4608
|
+
if (name && !schemas.includes(name)) {
|
|
4609
|
+
schemas.push(name);
|
|
4610
|
+
}
|
|
4611
|
+
}
|
|
4612
|
+
}
|
|
4613
|
+
return schemas;
|
|
4614
|
+
}
|
|
4615
|
+
function generateSamples(sourceText, schemaName, maxSamples) {
|
|
4616
|
+
const samples = [];
|
|
4617
|
+
const schemaBlock = extractSchemaBlock(sourceText, schemaName);
|
|
4618
|
+
if (!schemaBlock) return PRIMITIVE_SAMPLES.string?.slice(0, maxSamples) ?? [];
|
|
4619
|
+
if (/\.email\s*\(/.test(schemaBlock)) {
|
|
4620
|
+
samples.push(...EMAIL_SAMPLES);
|
|
4621
|
+
}
|
|
4622
|
+
if (/\.url\s*\(/.test(schemaBlock)) {
|
|
4623
|
+
samples.push(...URL_SAMPLES);
|
|
4624
|
+
}
|
|
4625
|
+
if (/\.uuid\s*\(/.test(schemaBlock)) {
|
|
4626
|
+
samples.push(...UUID_SAMPLES);
|
|
4627
|
+
}
|
|
4628
|
+
if (/string\s*\(/.test(schemaBlock)) {
|
|
4629
|
+
samples.push(...PRIMITIVE_SAMPLES.string ?? []);
|
|
4630
|
+
}
|
|
4631
|
+
if (/number\s*\(/.test(schemaBlock) || /\.int\s*\(/.test(schemaBlock)) {
|
|
4632
|
+
samples.push(...PRIMITIVE_SAMPLES.number ?? []);
|
|
4633
|
+
}
|
|
4634
|
+
if (/boolean\s*\(/.test(schemaBlock)) {
|
|
4635
|
+
samples.push(...PRIMITIVE_SAMPLES.boolean ?? []);
|
|
4636
|
+
}
|
|
4637
|
+
if (/date\s*\(/.test(schemaBlock)) {
|
|
4638
|
+
samples.push(...PRIMITIVE_SAMPLES.date ?? []);
|
|
4639
|
+
}
|
|
4640
|
+
if (/\.optional\s*\(/.test(schemaBlock) || /optional\s*\(/.test(schemaBlock)) {
|
|
4641
|
+
samples.push({ name: "undefined (optional)", input: void 0, expectedValid: true });
|
|
4642
|
+
}
|
|
4643
|
+
if (/\.nullable\s*\(/.test(schemaBlock) || /nullable\s*\(/.test(schemaBlock)) {
|
|
4644
|
+
samples.push({ name: "null (nullable)", input: null, expectedValid: true });
|
|
4645
|
+
}
|
|
4646
|
+
if (/\.min\s*\(\s*(\d+)/.test(schemaBlock)) {
|
|
4647
|
+
const minMatch = schemaBlock.match(/\.min\s*\(\s*(\d+)/);
|
|
4648
|
+
const minVal = minMatch ? Number.parseInt(minMatch[1] ?? "0", 10) : 0;
|
|
4649
|
+
samples.push({
|
|
4650
|
+
name: `below min (${minVal})`,
|
|
4651
|
+
input: minVal > 0 ? "a".repeat(minVal - 1) : "",
|
|
4652
|
+
expectedValid: false
|
|
4653
|
+
});
|
|
4654
|
+
}
|
|
4655
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4656
|
+
const unique = [];
|
|
4657
|
+
for (const s of samples) {
|
|
4658
|
+
if (!seen.has(s.name)) {
|
|
4659
|
+
seen.add(s.name);
|
|
4660
|
+
unique.push(s);
|
|
4661
|
+
}
|
|
4662
|
+
}
|
|
4663
|
+
return unique.slice(0, maxSamples);
|
|
4664
|
+
}
|
|
4665
|
+
function extractSchemaBlock(sourceText, schemaName) {
|
|
4666
|
+
const escapedName = schemaName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
4667
|
+
const pattern = new RegExp(
|
|
4668
|
+
`(?:const|let|var|export\\s+const)\\s+${escapedName}\\s*=\\s*([\\s\\S]*?)(?:;\\s*$|;\\s*(?:const|let|var|export|function|class|type|interface))`,
|
|
4669
|
+
"m"
|
|
4670
|
+
);
|
|
4671
|
+
const match = sourceText.match(pattern);
|
|
4672
|
+
return match?.[1] ?? null;
|
|
4673
|
+
}
|
|
4674
|
+
function createVerificationReport(from, to, results) {
|
|
4675
|
+
const totalSchemas = results.length;
|
|
4676
|
+
const overallParityScore = totalSchemas > 0 ? results.reduce((sum, r) => sum + r.parityScore, 0) / totalSchemas : 100;
|
|
4677
|
+
return {
|
|
4678
|
+
from,
|
|
4679
|
+
to,
|
|
4680
|
+
totalSchemas,
|
|
4681
|
+
results,
|
|
4682
|
+
overallParityScore: Math.round(overallParityScore * 100) / 100,
|
|
4683
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
4684
|
+
};
|
|
4685
|
+
}
|
|
4686
|
+
function formatVerificationReport(report) {
|
|
4687
|
+
const lines = [];
|
|
4688
|
+
lines.push(`
|
|
4689
|
+
Schema Verification Report: ${report.from} \u2192 ${report.to}`);
|
|
4690
|
+
lines.push("\u2500".repeat(50));
|
|
4691
|
+
for (const result of report.results) {
|
|
4692
|
+
const icon = result.parityScore === 100 ? "\u2713" : result.parityScore >= 80 ? "\u26A0" : "\u2717";
|
|
4693
|
+
lines.push(
|
|
4694
|
+
` ${icon} ${result.schemaName} \u2014 ${result.parityScore}% parity (${result.matchingSamples}/${result.totalSamples} samples)`
|
|
4695
|
+
);
|
|
4696
|
+
for (const mismatch of result.mismatches) {
|
|
4697
|
+
lines.push(
|
|
4698
|
+
` \u2514\u2500 ${mismatch.sampleName}: source=${mismatch.sourceResult.valid ? "valid" : "invalid"}, target=${mismatch.targetResult.valid ? "valid" : "invalid"}`
|
|
4699
|
+
);
|
|
4700
|
+
}
|
|
4701
|
+
}
|
|
4702
|
+
lines.push("\u2500".repeat(50));
|
|
4703
|
+
lines.push(`Overall Parity: ${report.overallParityScore}%`);
|
|
4704
|
+
lines.push("");
|
|
4705
|
+
return lines.join("\n");
|
|
4706
|
+
}
|
|
4707
|
+
|
|
4137
4708
|
// src/standard-schema.ts
|
|
4138
4709
|
import { existsSync as existsSync10, readFileSync as readFileSync10 } from "fs";
|
|
4139
4710
|
import { join as join10 } from "path";
|
|
@@ -4602,11 +5173,16 @@ var TypeDedupDetector = class {
|
|
|
4602
5173
|
}
|
|
4603
5174
|
};
|
|
4604
5175
|
export {
|
|
5176
|
+
AUDIT_LOG_FILE,
|
|
4605
5177
|
ApprovalManager,
|
|
5178
|
+
BACKUP_DIR,
|
|
4606
5179
|
BehavioralWarningAnalyzer,
|
|
4607
5180
|
BundleEstimator,
|
|
5181
|
+
CONFIG_FILE_NAMES,
|
|
4608
5182
|
CompatibilityAnalyzer,
|
|
4609
5183
|
ComplexityEstimator,
|
|
5184
|
+
DEFAULT_CONFIG_FILE,
|
|
5185
|
+
DeadSchemaDetector,
|
|
4610
5186
|
DetailedAnalyzer,
|
|
4611
5187
|
DriftDetector,
|
|
4612
5188
|
EcosystemAnalyzer,
|
|
@@ -4615,16 +5191,22 @@ export {
|
|
|
4615
5191
|
GovernanceEngine,
|
|
4616
5192
|
GovernanceFixer,
|
|
4617
5193
|
GraphExporter,
|
|
5194
|
+
INCREMENTAL_STATE_FILE,
|
|
5195
|
+
ImportDeduplicator,
|
|
4618
5196
|
IncrementalTracker,
|
|
4619
5197
|
MigrationAuditLog,
|
|
4620
5198
|
MigrationChain,
|
|
4621
5199
|
MonorepoResolver,
|
|
5200
|
+
PENDING_DIR,
|
|
4622
5201
|
PackageUpdater,
|
|
4623
5202
|
PerformanceAnalyzer,
|
|
4624
5203
|
PluginLoader,
|
|
5204
|
+
SCHEMASHIFT_DIR,
|
|
5205
|
+
SCHEMA_SNAPSHOT_FILE,
|
|
4625
5206
|
SchemaAnalyzer,
|
|
4626
5207
|
SchemaDependencyResolver,
|
|
4627
5208
|
StandardSchemaAdvisor,
|
|
5209
|
+
TESTS_DIR,
|
|
4628
5210
|
TestScaffolder,
|
|
4629
5211
|
TransformEngine,
|
|
4630
5212
|
TypeDedupDetector,
|
|
@@ -4632,10 +5214,14 @@ export {
|
|
|
4632
5214
|
buildCallChain,
|
|
4633
5215
|
computeParallelBatches,
|
|
4634
5216
|
conditionalValidation,
|
|
5217
|
+
createVerificationReport,
|
|
4635
5218
|
dependentFields,
|
|
4636
5219
|
detectFormLibraries,
|
|
4637
5220
|
detectSchemaLibrary,
|
|
4638
5221
|
detectStandardSchema,
|
|
5222
|
+
extractSchemaNames,
|
|
5223
|
+
formatVerificationReport,
|
|
5224
|
+
generateSamples,
|
|
4639
5225
|
getAllMigrationTemplates,
|
|
4640
5226
|
getGovernanceTemplate,
|
|
4641
5227
|
getGovernanceTemplateNames,
|