@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.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, ".schemashift", "pending");
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
- const content = readFileSync(filePath, "utf-8");
192
- return JSON.parse(content);
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
- const content = readFileSync(join(this.pendingDir, file), "utf-8");
205
- const request = JSON.parse(content);
206
- if (!status || request.status === status) {
207
- requests.push(request);
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, AUDIT_DIR);
386
- this.logPath = join2(this.logDir, AUDIT_FILE);
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
- return JSON.parse(content);
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 majorMatch = version.match(/(\d+)/);
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 majorMatch = version.match(/(\d+)/);
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 majorMatch = version.match(/(\d+)/);
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: ".schemashift-backup" },
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, SNAPSHOT_DIR);
2363
- this.snapshotPath = join7(this.snapshotDir, SNAPSHOT_FILE);
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
- if (!new RegExp(pattern).test(schemaName)) {
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, STATE_DIR);
3540
- this.statePath = join8(this.stateDir, STATE_FILE);
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
- return JSON.parse(readFileSync8(this.statePath, "utf-8"));
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
- const payload = JSON.stringify(event);
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: err instanceof Error ? err.message : String(err)
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,