@schemashift/core 0.11.0 → 0.12.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 CHANGED
@@ -60,10 +60,14 @@ __export(index_exports, {
60
60
  buildCallChain: () => buildCallChain,
61
61
  computeParallelBatches: () => computeParallelBatches,
62
62
  conditionalValidation: () => conditionalValidation,
63
+ createVerificationReport: () => createVerificationReport,
63
64
  dependentFields: () => dependentFields,
64
65
  detectFormLibraries: () => detectFormLibraries,
65
66
  detectSchemaLibrary: () => detectSchemaLibrary,
66
67
  detectStandardSchema: () => detectStandardSchema,
68
+ extractSchemaNames: () => extractSchemaNames,
69
+ formatVerificationReport: () => formatVerificationReport,
70
+ generateSamples: () => generateSamples,
67
71
  getAllMigrationTemplates: () => getAllMigrationTemplates,
68
72
  getGovernanceTemplate: () => getGovernanceTemplate,
69
73
  getGovernanceTemplateNames: () => getGovernanceTemplateNames,
@@ -594,6 +598,88 @@ var MigrationAuditLog = class {
594
598
  clear() {
595
599
  this.write({ version: AUDIT_VERSION, entries: [] });
596
600
  }
601
+ /**
602
+ * Export a compliance report in SOC2 or HIPAA format.
603
+ */
604
+ exportComplianceReport(format) {
605
+ const log = this.read();
606
+ const summary = this.getSummary();
607
+ if (format === "soc2") {
608
+ return this.generateSoc2Report(log, summary);
609
+ }
610
+ return this.generateHipaaReport(log, summary);
611
+ }
612
+ generateSoc2Report(log, summary) {
613
+ const sections = [];
614
+ const now = (/* @__PURE__ */ new Date()).toISOString();
615
+ sections.push("# SOC2 Compliance Report \u2014 Schema Migration");
616
+ sections.push(`Generated: ${now}`);
617
+ sections.push("");
618
+ sections.push("## Change Control Summary");
619
+ sections.push(`- Total Migrations: ${summary.totalMigrations}`);
620
+ sections.push(`- Total Files Processed: ${summary.totalFiles}`);
621
+ sections.push(`- Successful: ${summary.successCount}`);
622
+ sections.push(`- Failed: ${summary.failureCount}`);
623
+ sections.push(`- Migration Paths: ${summary.migrationPaths.join(", ")}`);
624
+ sections.push("");
625
+ sections.push("## Change Control Entries");
626
+ for (const entry of log.entries) {
627
+ sections.push("");
628
+ sections.push(`### ${entry.filePath}`);
629
+ sections.push(`- Change ID: ${entry.migrationId}`);
630
+ sections.push(`- Timestamp: ${entry.timestamp}`);
631
+ sections.push(`- Action: ${entry.action}`);
632
+ sections.push(`- Migration: ${entry.from} \u2192 ${entry.to}`);
633
+ sections.push(`- Status: ${entry.success ? "Success" : "Failed"}`);
634
+ sections.push(`- Implementer: ${entry.user || "Unknown"}`);
635
+ sections.push(`- Before Hash: ${entry.beforeHash}`);
636
+ if (entry.afterHash) sections.push(`- After Hash: ${entry.afterHash}`);
637
+ sections.push(`- Warnings: ${entry.warningCount}`);
638
+ sections.push(`- Errors: ${entry.errorCount}`);
639
+ if (entry.riskScore !== void 0) sections.push(`- Risk Score: ${entry.riskScore}`);
640
+ if (entry.metadata?.ciProvider) sections.push(`- CI Provider: ${entry.metadata.ciProvider}`);
641
+ if (entry.metadata?.gitCommit) sections.push(`- Git Commit: ${entry.metadata.gitCommit}`);
642
+ if (entry.metadata?.gitBranch) sections.push(`- Git Branch: ${entry.metadata.gitBranch}`);
643
+ }
644
+ sections.push("");
645
+ sections.push("## Rollback Procedure");
646
+ sections.push("SchemaShift maintains automatic backups in `.schemashift/backups/`.");
647
+ sections.push("Use `schemashift rollback [backupId]` to restore files from any backup.");
648
+ sections.push("");
649
+ return sections.join("\n");
650
+ }
651
+ generateHipaaReport(log, summary) {
652
+ const sections = [];
653
+ const now = (/* @__PURE__ */ new Date()).toISOString();
654
+ sections.push("# HIPAA Compliance Audit Trail \u2014 Schema Migration");
655
+ sections.push(`Generated: ${now}`);
656
+ sections.push("");
657
+ sections.push("## Data Transformation Summary");
658
+ sections.push(`- Total Transformations: ${summary.totalFiles}`);
659
+ sections.push(`- Successful: ${summary.successCount}`);
660
+ sections.push(`- Failed: ${summary.failureCount}`);
661
+ sections.push("");
662
+ sections.push("## Integrity Verification");
663
+ for (const entry of log.entries) {
664
+ sections.push("");
665
+ sections.push(`### ${entry.filePath}`);
666
+ sections.push(`- Timestamp: ${entry.timestamp}`);
667
+ sections.push(`- User: ${entry.user || "Unknown"}`);
668
+ sections.push(`- Action: ${entry.action} (${entry.from} \u2192 ${entry.to})`);
669
+ sections.push(`- Integrity Before: SHA256:${entry.beforeHash}`);
670
+ if (entry.afterHash) sections.push(`- Integrity After: SHA256:${entry.afterHash}`);
671
+ sections.push(`- Status: ${entry.success ? "Completed" : "Failed"}`);
672
+ if (entry.metadata?.hostname) sections.push(`- Host: ${entry.metadata.hostname}`);
673
+ if (entry.metadata?.nodeVersion)
674
+ sections.push(`- Runtime: Node.js ${entry.metadata.nodeVersion}`);
675
+ }
676
+ sections.push("");
677
+ sections.push("## Access Control");
678
+ const users = [...new Set(log.entries.map((e) => e.user).filter(Boolean))];
679
+ sections.push(`- Users Who Performed Migrations: ${users.join(", ") || "Unknown"}`);
680
+ sections.push("");
681
+ return sections.join("\n");
682
+ }
597
683
  collectMetadata() {
598
684
  return {
599
685
  hostname: process.env.HOSTNAME || void 0,
@@ -1726,6 +1812,25 @@ var ComplexityEstimator = class {
1726
1812
  riskAreas
1727
1813
  };
1728
1814
  }
1815
+ estimateDuration(estimate) {
1816
+ const EFFORT_RANGES = {
1817
+ trivial: { label: "1\u20135 minutes", range: [1, 5] },
1818
+ low: { label: "5\u201315 minutes", range: [5, 15] },
1819
+ moderate: { label: "15\u201345 minutes", range: [15, 45] },
1820
+ high: { label: "1\u20133 hours", range: [60, 180] },
1821
+ extreme: { label: "3\u20138 hours", range: [180, 480] }
1822
+ };
1823
+ const base = EFFORT_RANGES[estimate.effort];
1824
+ const fileMultiplier = Math.max(1, Math.log2(estimate.totalFiles + 1));
1825
+ const low = Math.round(base.range[0] * fileMultiplier);
1826
+ const high = Math.round(base.range[1] * fileMultiplier);
1827
+ if (high >= 120) {
1828
+ const lowHours = Math.round(low / 60 * 10) / 10;
1829
+ const highHours = Math.round(high / 60 * 10) / 10;
1830
+ return { label: `${lowHours}\u2013${highHours} hours`, rangeMinutes: [low, high] };
1831
+ }
1832
+ return { label: `${low}\u2013${high} minutes`, rangeMinutes: [low, high] };
1833
+ }
1729
1834
  calculateEffort(totalSchemas, advancedCount, hasDeepDU) {
1730
1835
  if (totalSchemas >= 500 && hasDeepDU) return "extreme";
1731
1836
  if (totalSchemas >= 200 || advancedCount >= 20) return "high";
@@ -3689,6 +3794,22 @@ var IncrementalTracker = class {
3689
3794
  percent
3690
3795
  };
3691
3796
  }
3797
+ /**
3798
+ * Get a canary batch — a percentage of remaining files, sorted simplest first.
3799
+ * Used for phased rollouts where you migrate a small batch, verify, then continue.
3800
+ */
3801
+ getCanaryBatch(percent, fileSizes) {
3802
+ const state = this.getState();
3803
+ if (!state) return [];
3804
+ const count = Math.max(1, Math.ceil(state.remainingFiles.length * (percent / 100)));
3805
+ if (fileSizes) {
3806
+ const sorted = [...state.remainingFiles].sort((a, b) => {
3807
+ return (fileSizes.get(a) ?? 0) - (fileSizes.get(b) ?? 0);
3808
+ });
3809
+ return sorted.slice(0, count);
3810
+ }
3811
+ return state.remainingFiles.slice(0, count);
3812
+ }
3692
3813
  clear() {
3693
3814
  if ((0, import_node_fs8.existsSync)(this.statePath)) {
3694
3815
  (0, import_node_fs8.unlinkSync)(this.statePath);
@@ -3903,11 +4024,111 @@ var WebhookNotifier = class {
3903
4024
  }
3904
4025
  return results;
3905
4026
  }
4027
+ /**
4028
+ * Format event as Slack Block Kit message.
4029
+ */
4030
+ formatSlackPayload(event) {
4031
+ const emoji = this.getEventEmoji(event.type);
4032
+ const title = this.getEventTitle(event.type);
4033
+ const details = event.details;
4034
+ const blocks = [
4035
+ {
4036
+ type: "header",
4037
+ text: { type: "plain_text", text: `${emoji} ${title}`, emoji: true }
4038
+ },
4039
+ {
4040
+ type: "section",
4041
+ fields: Object.entries(details).map(([key, value]) => ({
4042
+ type: "mrkdwn",
4043
+ text: `*${key}:* ${String(value)}`
4044
+ }))
4045
+ },
4046
+ {
4047
+ type: "context",
4048
+ elements: [
4049
+ {
4050
+ type: "mrkdwn",
4051
+ text: `SchemaShift | ${event.timestamp}${event.project ? ` | ${event.project}` : ""}`
4052
+ }
4053
+ ]
4054
+ }
4055
+ ];
4056
+ return { blocks };
4057
+ }
4058
+ /**
4059
+ * Format event as Microsoft Teams Adaptive Card.
4060
+ */
4061
+ formatTeamsPayload(event) {
4062
+ const title = this.getEventTitle(event.type);
4063
+ const details = event.details;
4064
+ const facts = Object.entries(details).map(([key, value]) => ({
4065
+ title: key,
4066
+ value: String(value)
4067
+ }));
4068
+ return {
4069
+ type: "message",
4070
+ attachments: [
4071
+ {
4072
+ contentType: "application/vnd.microsoft.card.adaptive",
4073
+ content: {
4074
+ $schema: "http://adaptivecards.io/schemas/adaptive-card.json",
4075
+ type: "AdaptiveCard",
4076
+ version: "1.4",
4077
+ body: [
4078
+ {
4079
+ type: "TextBlock",
4080
+ text: title,
4081
+ weight: "Bolder",
4082
+ size: "Medium"
4083
+ },
4084
+ {
4085
+ type: "FactSet",
4086
+ facts
4087
+ },
4088
+ {
4089
+ type: "TextBlock",
4090
+ text: `SchemaShift | ${event.timestamp}`,
4091
+ isSubtle: true,
4092
+ size: "Small"
4093
+ }
4094
+ ]
4095
+ }
4096
+ }
4097
+ ]
4098
+ };
4099
+ }
4100
+ getEventEmoji(type) {
4101
+ const emojis = {
4102
+ migration_started: "\u{1F504}",
4103
+ migration_completed: "\u2705",
4104
+ migration_failed: "\u274C",
4105
+ governance_violation: "\u26A0\uFE0F",
4106
+ drift_detected: "\u{1F50D}"
4107
+ };
4108
+ return emojis[type];
4109
+ }
4110
+ getEventTitle(type) {
4111
+ const titles = {
4112
+ migration_started: "Migration Started",
4113
+ migration_completed: "Migration Completed",
4114
+ migration_failed: "Migration Failed",
4115
+ governance_violation: "Governance Violation",
4116
+ drift_detected: "Schema Drift Detected"
4117
+ };
4118
+ return titles[type];
4119
+ }
3906
4120
  /**
3907
4121
  * Send event to a single webhook endpoint.
3908
4122
  */
3909
4123
  async sendToWebhook(webhook, event) {
3910
- const payload = JSON.stringify(event);
4124
+ let payload;
4125
+ if (webhook.type === "slack") {
4126
+ payload = JSON.stringify(this.formatSlackPayload(event));
4127
+ } else if (webhook.type === "teams") {
4128
+ payload = JSON.stringify(this.formatTeamsPayload(event));
4129
+ } else {
4130
+ payload = JSON.stringify(event);
4131
+ }
3911
4132
  const headers = {
3912
4133
  "Content-Type": "application/json",
3913
4134
  "User-Agent": "SchemaShift-Webhook/1.0",
@@ -4223,6 +4444,161 @@ var PluginLoader = class {
4223
4444
  }
4224
4445
  };
4225
4446
 
4447
+ // src/schema-verifier.ts
4448
+ var PRIMITIVE_SAMPLES = {
4449
+ string: [
4450
+ { name: "empty string", input: "", expectedValid: true },
4451
+ { name: "normal string", input: "hello world", expectedValid: true },
4452
+ { name: "number as string", input: "12345", expectedValid: true },
4453
+ { name: "null input", input: null, expectedValid: false },
4454
+ { name: "number input", input: 42, expectedValid: false },
4455
+ { name: "boolean input", input: true, expectedValid: false },
4456
+ { name: "undefined input", input: void 0, expectedValid: false }
4457
+ ],
4458
+ number: [
4459
+ { name: "zero", input: 0, expectedValid: true },
4460
+ { name: "positive int", input: 42, expectedValid: true },
4461
+ { name: "negative int", input: -1, expectedValid: true },
4462
+ { name: "float", input: 3.14, expectedValid: true },
4463
+ { name: "string input", input: "hello", expectedValid: false },
4464
+ { name: "null input", input: null, expectedValid: false },
4465
+ { name: "NaN input", input: Number.NaN, expectedValid: false }
4466
+ ],
4467
+ boolean: [
4468
+ { name: "true", input: true, expectedValid: true },
4469
+ { name: "false", input: false, expectedValid: true },
4470
+ { name: "string input", input: "true", expectedValid: false },
4471
+ { name: "number input", input: 1, expectedValid: false },
4472
+ { name: "null input", input: null, expectedValid: false }
4473
+ ],
4474
+ date: [
4475
+ { name: "valid date", input: /* @__PURE__ */ new Date("2024-01-01"), expectedValid: true },
4476
+ { name: "string input", input: "2024-01-01", expectedValid: false },
4477
+ { name: "null input", input: null, expectedValid: false }
4478
+ ]
4479
+ };
4480
+ var EMAIL_SAMPLES = [
4481
+ { name: "valid email", input: "test@example.com", expectedValid: true },
4482
+ { name: "invalid email", input: "not-an-email", expectedValid: false },
4483
+ { name: "empty string", input: "", expectedValid: false }
4484
+ ];
4485
+ var URL_SAMPLES = [
4486
+ { name: "valid url", input: "https://example.com", expectedValid: true },
4487
+ { name: "invalid url", input: "not a url", expectedValid: false }
4488
+ ];
4489
+ var UUID_SAMPLES = [
4490
+ { name: "valid uuid", input: "550e8400-e29b-41d4-a716-446655440000", expectedValid: true },
4491
+ { name: "invalid uuid", input: "not-a-uuid", expectedValid: false }
4492
+ ];
4493
+ function extractSchemaNames(sourceText) {
4494
+ const schemas = [];
4495
+ const patterns = [
4496
+ /(?:const|let|var)\s+(\w+)\s*=\s*(?:z\.|yup\.|Joi\.|v\.|t\.|S\.|type\(|object\(|string\()/g,
4497
+ /export\s+(?:const|let|var)\s+(\w+)\s*=\s*(?:z\.|yup\.|Joi\.|v\.|t\.|S\.|type\(|object\(|string\()/g
4498
+ ];
4499
+ for (const pattern of patterns) {
4500
+ for (const match of sourceText.matchAll(pattern)) {
4501
+ const name = match[1];
4502
+ if (name && !schemas.includes(name)) {
4503
+ schemas.push(name);
4504
+ }
4505
+ }
4506
+ }
4507
+ return schemas;
4508
+ }
4509
+ function generateSamples(sourceText, schemaName, maxSamples) {
4510
+ const samples = [];
4511
+ const schemaBlock = extractSchemaBlock(sourceText, schemaName);
4512
+ if (!schemaBlock) return PRIMITIVE_SAMPLES.string?.slice(0, maxSamples) ?? [];
4513
+ if (/\.email\s*\(/.test(schemaBlock)) {
4514
+ samples.push(...EMAIL_SAMPLES);
4515
+ }
4516
+ if (/\.url\s*\(/.test(schemaBlock)) {
4517
+ samples.push(...URL_SAMPLES);
4518
+ }
4519
+ if (/\.uuid\s*\(/.test(schemaBlock)) {
4520
+ samples.push(...UUID_SAMPLES);
4521
+ }
4522
+ if (/string\s*\(/.test(schemaBlock)) {
4523
+ samples.push(...PRIMITIVE_SAMPLES.string ?? []);
4524
+ }
4525
+ if (/number\s*\(/.test(schemaBlock) || /\.int\s*\(/.test(schemaBlock)) {
4526
+ samples.push(...PRIMITIVE_SAMPLES.number ?? []);
4527
+ }
4528
+ if (/boolean\s*\(/.test(schemaBlock)) {
4529
+ samples.push(...PRIMITIVE_SAMPLES.boolean ?? []);
4530
+ }
4531
+ if (/date\s*\(/.test(schemaBlock)) {
4532
+ samples.push(...PRIMITIVE_SAMPLES.date ?? []);
4533
+ }
4534
+ if (/\.optional\s*\(/.test(schemaBlock) || /optional\s*\(/.test(schemaBlock)) {
4535
+ samples.push({ name: "undefined (optional)", input: void 0, expectedValid: true });
4536
+ }
4537
+ if (/\.nullable\s*\(/.test(schemaBlock) || /nullable\s*\(/.test(schemaBlock)) {
4538
+ samples.push({ name: "null (nullable)", input: null, expectedValid: true });
4539
+ }
4540
+ if (/\.min\s*\(\s*(\d+)/.test(schemaBlock)) {
4541
+ const minMatch = schemaBlock.match(/\.min\s*\(\s*(\d+)/);
4542
+ const minVal = minMatch ? Number.parseInt(minMatch[1] ?? "0", 10) : 0;
4543
+ samples.push({
4544
+ name: `below min (${minVal})`,
4545
+ input: minVal > 0 ? "a".repeat(minVal - 1) : "",
4546
+ expectedValid: false
4547
+ });
4548
+ }
4549
+ const seen = /* @__PURE__ */ new Set();
4550
+ const unique = [];
4551
+ for (const s of samples) {
4552
+ if (!seen.has(s.name)) {
4553
+ seen.add(s.name);
4554
+ unique.push(s);
4555
+ }
4556
+ }
4557
+ return unique.slice(0, maxSamples);
4558
+ }
4559
+ function extractSchemaBlock(sourceText, schemaName) {
4560
+ const escapedName = schemaName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
4561
+ const pattern = new RegExp(
4562
+ `(?:const|let|var|export\\s+const)\\s+${escapedName}\\s*=\\s*([\\s\\S]*?)(?:;\\s*$|;\\s*(?:const|let|var|export|function|class|type|interface))`,
4563
+ "m"
4564
+ );
4565
+ const match = sourceText.match(pattern);
4566
+ return match?.[1] ?? null;
4567
+ }
4568
+ function createVerificationReport(from, to, results) {
4569
+ const totalSchemas = results.length;
4570
+ const overallParityScore = totalSchemas > 0 ? results.reduce((sum, r) => sum + r.parityScore, 0) / totalSchemas : 100;
4571
+ return {
4572
+ from,
4573
+ to,
4574
+ totalSchemas,
4575
+ results,
4576
+ overallParityScore: Math.round(overallParityScore * 100) / 100,
4577
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
4578
+ };
4579
+ }
4580
+ function formatVerificationReport(report) {
4581
+ const lines = [];
4582
+ lines.push(`
4583
+ Schema Verification Report: ${report.from} \u2192 ${report.to}`);
4584
+ lines.push("\u2500".repeat(50));
4585
+ for (const result of report.results) {
4586
+ const icon = result.parityScore === 100 ? "\u2713" : result.parityScore >= 80 ? "\u26A0" : "\u2717";
4587
+ lines.push(
4588
+ ` ${icon} ${result.schemaName} \u2014 ${result.parityScore}% parity (${result.matchingSamples}/${result.totalSamples} samples)`
4589
+ );
4590
+ for (const mismatch of result.mismatches) {
4591
+ lines.push(
4592
+ ` \u2514\u2500 ${mismatch.sampleName}: source=${mismatch.sourceResult.valid ? "valid" : "invalid"}, target=${mismatch.targetResult.valid ? "valid" : "invalid"}`
4593
+ );
4594
+ }
4595
+ }
4596
+ lines.push("\u2500".repeat(50));
4597
+ lines.push(`Overall Parity: ${report.overallParityScore}%`);
4598
+ lines.push("");
4599
+ return lines.join("\n");
4600
+ }
4601
+
4226
4602
  // src/standard-schema.ts
4227
4603
  var import_node_fs10 = require("fs");
4228
4604
  var import_node_path10 = require("path");
@@ -4722,10 +5098,14 @@ var TypeDedupDetector = class {
4722
5098
  buildCallChain,
4723
5099
  computeParallelBatches,
4724
5100
  conditionalValidation,
5101
+ createVerificationReport,
4725
5102
  dependentFields,
4726
5103
  detectFormLibraries,
4727
5104
  detectSchemaLibrary,
4728
5105
  detectStandardSchema,
5106
+ extractSchemaNames,
5107
+ formatVerificationReport,
5108
+ generateSamples,
4729
5109
  getAllMigrationTemplates,
4730
5110
  getGovernanceTemplate,
4731
5111
  getGovernanceTemplateNames,