@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.js CHANGED
@@ -505,6 +505,88 @@ var MigrationAuditLog = class {
505
505
  clear() {
506
506
  this.write({ version: AUDIT_VERSION, entries: [] });
507
507
  }
508
+ /**
509
+ * Export a compliance report in SOC2 or HIPAA format.
510
+ */
511
+ exportComplianceReport(format) {
512
+ const log = this.read();
513
+ const summary = this.getSummary();
514
+ if (format === "soc2") {
515
+ return this.generateSoc2Report(log, summary);
516
+ }
517
+ return this.generateHipaaReport(log, summary);
518
+ }
519
+ generateSoc2Report(log, summary) {
520
+ const sections = [];
521
+ const now = (/* @__PURE__ */ new Date()).toISOString();
522
+ sections.push("# SOC2 Compliance Report \u2014 Schema Migration");
523
+ sections.push(`Generated: ${now}`);
524
+ sections.push("");
525
+ sections.push("## Change Control Summary");
526
+ sections.push(`- Total Migrations: ${summary.totalMigrations}`);
527
+ sections.push(`- Total Files Processed: ${summary.totalFiles}`);
528
+ sections.push(`- Successful: ${summary.successCount}`);
529
+ sections.push(`- Failed: ${summary.failureCount}`);
530
+ sections.push(`- Migration Paths: ${summary.migrationPaths.join(", ")}`);
531
+ sections.push("");
532
+ sections.push("## Change Control Entries");
533
+ for (const entry of log.entries) {
534
+ sections.push("");
535
+ sections.push(`### ${entry.filePath}`);
536
+ sections.push(`- Change ID: ${entry.migrationId}`);
537
+ sections.push(`- Timestamp: ${entry.timestamp}`);
538
+ sections.push(`- Action: ${entry.action}`);
539
+ sections.push(`- Migration: ${entry.from} \u2192 ${entry.to}`);
540
+ sections.push(`- Status: ${entry.success ? "Success" : "Failed"}`);
541
+ sections.push(`- Implementer: ${entry.user || "Unknown"}`);
542
+ sections.push(`- Before Hash: ${entry.beforeHash}`);
543
+ if (entry.afterHash) sections.push(`- After Hash: ${entry.afterHash}`);
544
+ sections.push(`- Warnings: ${entry.warningCount}`);
545
+ sections.push(`- Errors: ${entry.errorCount}`);
546
+ if (entry.riskScore !== void 0) sections.push(`- Risk Score: ${entry.riskScore}`);
547
+ if (entry.metadata?.ciProvider) sections.push(`- CI Provider: ${entry.metadata.ciProvider}`);
548
+ if (entry.metadata?.gitCommit) sections.push(`- Git Commit: ${entry.metadata.gitCommit}`);
549
+ if (entry.metadata?.gitBranch) sections.push(`- Git Branch: ${entry.metadata.gitBranch}`);
550
+ }
551
+ sections.push("");
552
+ sections.push("## Rollback Procedure");
553
+ sections.push("SchemaShift maintains automatic backups in `.schemashift/backups/`.");
554
+ sections.push("Use `schemashift rollback [backupId]` to restore files from any backup.");
555
+ sections.push("");
556
+ return sections.join("\n");
557
+ }
558
+ generateHipaaReport(log, summary) {
559
+ const sections = [];
560
+ const now = (/* @__PURE__ */ new Date()).toISOString();
561
+ sections.push("# HIPAA Compliance Audit Trail \u2014 Schema Migration");
562
+ sections.push(`Generated: ${now}`);
563
+ sections.push("");
564
+ sections.push("## Data Transformation Summary");
565
+ sections.push(`- Total Transformations: ${summary.totalFiles}`);
566
+ sections.push(`- Successful: ${summary.successCount}`);
567
+ sections.push(`- Failed: ${summary.failureCount}`);
568
+ sections.push("");
569
+ sections.push("## Integrity Verification");
570
+ for (const entry of log.entries) {
571
+ sections.push("");
572
+ sections.push(`### ${entry.filePath}`);
573
+ sections.push(`- Timestamp: ${entry.timestamp}`);
574
+ sections.push(`- User: ${entry.user || "Unknown"}`);
575
+ sections.push(`- Action: ${entry.action} (${entry.from} \u2192 ${entry.to})`);
576
+ sections.push(`- Integrity Before: SHA256:${entry.beforeHash}`);
577
+ if (entry.afterHash) sections.push(`- Integrity After: SHA256:${entry.afterHash}`);
578
+ sections.push(`- Status: ${entry.success ? "Completed" : "Failed"}`);
579
+ if (entry.metadata?.hostname) sections.push(`- Host: ${entry.metadata.hostname}`);
580
+ if (entry.metadata?.nodeVersion)
581
+ sections.push(`- Runtime: Node.js ${entry.metadata.nodeVersion}`);
582
+ }
583
+ sections.push("");
584
+ sections.push("## Access Control");
585
+ const users = [...new Set(log.entries.map((e) => e.user).filter(Boolean))];
586
+ sections.push(`- Users Who Performed Migrations: ${users.join(", ") || "Unknown"}`);
587
+ sections.push("");
588
+ return sections.join("\n");
589
+ }
508
590
  collectMetadata() {
509
591
  return {
510
592
  hostname: process.env.HOSTNAME || void 0,
@@ -1637,6 +1719,25 @@ var ComplexityEstimator = class {
1637
1719
  riskAreas
1638
1720
  };
1639
1721
  }
1722
+ estimateDuration(estimate) {
1723
+ const EFFORT_RANGES = {
1724
+ trivial: { label: "1\u20135 minutes", range: [1, 5] },
1725
+ low: { label: "5\u201315 minutes", range: [5, 15] },
1726
+ moderate: { label: "15\u201345 minutes", range: [15, 45] },
1727
+ high: { label: "1\u20133 hours", range: [60, 180] },
1728
+ extreme: { label: "3\u20138 hours", range: [180, 480] }
1729
+ };
1730
+ const base = EFFORT_RANGES[estimate.effort];
1731
+ const fileMultiplier = Math.max(1, Math.log2(estimate.totalFiles + 1));
1732
+ const low = Math.round(base.range[0] * fileMultiplier);
1733
+ const high = Math.round(base.range[1] * fileMultiplier);
1734
+ if (high >= 120) {
1735
+ const lowHours = Math.round(low / 60 * 10) / 10;
1736
+ const highHours = Math.round(high / 60 * 10) / 10;
1737
+ return { label: `${lowHours}\u2013${highHours} hours`, rangeMinutes: [low, high] };
1738
+ }
1739
+ return { label: `${low}\u2013${high} minutes`, rangeMinutes: [low, high] };
1740
+ }
1640
1741
  calculateEffort(totalSchemas, advancedCount, hasDeepDU) {
1641
1742
  if (totalSchemas >= 500 && hasDeepDU) return "extreme";
1642
1743
  if (totalSchemas >= 200 || advancedCount >= 20) return "high";
@@ -3600,6 +3701,22 @@ var IncrementalTracker = class {
3600
3701
  percent
3601
3702
  };
3602
3703
  }
3704
+ /**
3705
+ * Get a canary batch — a percentage of remaining files, sorted simplest first.
3706
+ * Used for phased rollouts where you migrate a small batch, verify, then continue.
3707
+ */
3708
+ getCanaryBatch(percent, fileSizes) {
3709
+ const state = this.getState();
3710
+ if (!state) return [];
3711
+ const count = Math.max(1, Math.ceil(state.remainingFiles.length * (percent / 100)));
3712
+ if (fileSizes) {
3713
+ const sorted = [...state.remainingFiles].sort((a, b) => {
3714
+ return (fileSizes.get(a) ?? 0) - (fileSizes.get(b) ?? 0);
3715
+ });
3716
+ return sorted.slice(0, count);
3717
+ }
3718
+ return state.remainingFiles.slice(0, count);
3719
+ }
3603
3720
  clear() {
3604
3721
  if (existsSync8(this.statePath)) {
3605
3722
  unlinkSync(this.statePath);
@@ -3814,11 +3931,111 @@ var WebhookNotifier = class {
3814
3931
  }
3815
3932
  return results;
3816
3933
  }
3934
+ /**
3935
+ * Format event as Slack Block Kit message.
3936
+ */
3937
+ formatSlackPayload(event) {
3938
+ const emoji = this.getEventEmoji(event.type);
3939
+ const title = this.getEventTitle(event.type);
3940
+ const details = event.details;
3941
+ const blocks = [
3942
+ {
3943
+ type: "header",
3944
+ text: { type: "plain_text", text: `${emoji} ${title}`, emoji: true }
3945
+ },
3946
+ {
3947
+ type: "section",
3948
+ fields: Object.entries(details).map(([key, value]) => ({
3949
+ type: "mrkdwn",
3950
+ text: `*${key}:* ${String(value)}`
3951
+ }))
3952
+ },
3953
+ {
3954
+ type: "context",
3955
+ elements: [
3956
+ {
3957
+ type: "mrkdwn",
3958
+ text: `SchemaShift | ${event.timestamp}${event.project ? ` | ${event.project}` : ""}`
3959
+ }
3960
+ ]
3961
+ }
3962
+ ];
3963
+ return { blocks };
3964
+ }
3965
+ /**
3966
+ * Format event as Microsoft Teams Adaptive Card.
3967
+ */
3968
+ formatTeamsPayload(event) {
3969
+ const title = this.getEventTitle(event.type);
3970
+ const details = event.details;
3971
+ const facts = Object.entries(details).map(([key, value]) => ({
3972
+ title: key,
3973
+ value: String(value)
3974
+ }));
3975
+ return {
3976
+ type: "message",
3977
+ attachments: [
3978
+ {
3979
+ contentType: "application/vnd.microsoft.card.adaptive",
3980
+ content: {
3981
+ $schema: "http://adaptivecards.io/schemas/adaptive-card.json",
3982
+ type: "AdaptiveCard",
3983
+ version: "1.4",
3984
+ body: [
3985
+ {
3986
+ type: "TextBlock",
3987
+ text: title,
3988
+ weight: "Bolder",
3989
+ size: "Medium"
3990
+ },
3991
+ {
3992
+ type: "FactSet",
3993
+ facts
3994
+ },
3995
+ {
3996
+ type: "TextBlock",
3997
+ text: `SchemaShift | ${event.timestamp}`,
3998
+ isSubtle: true,
3999
+ size: "Small"
4000
+ }
4001
+ ]
4002
+ }
4003
+ }
4004
+ ]
4005
+ };
4006
+ }
4007
+ getEventEmoji(type) {
4008
+ const emojis = {
4009
+ migration_started: "\u{1F504}",
4010
+ migration_completed: "\u2705",
4011
+ migration_failed: "\u274C",
4012
+ governance_violation: "\u26A0\uFE0F",
4013
+ drift_detected: "\u{1F50D}"
4014
+ };
4015
+ return emojis[type];
4016
+ }
4017
+ getEventTitle(type) {
4018
+ const titles = {
4019
+ migration_started: "Migration Started",
4020
+ migration_completed: "Migration Completed",
4021
+ migration_failed: "Migration Failed",
4022
+ governance_violation: "Governance Violation",
4023
+ drift_detected: "Schema Drift Detected"
4024
+ };
4025
+ return titles[type];
4026
+ }
3817
4027
  /**
3818
4028
  * Send event to a single webhook endpoint.
3819
4029
  */
3820
4030
  async sendToWebhook(webhook, event) {
3821
- const payload = JSON.stringify(event);
4031
+ let payload;
4032
+ if (webhook.type === "slack") {
4033
+ payload = JSON.stringify(this.formatSlackPayload(event));
4034
+ } else if (webhook.type === "teams") {
4035
+ payload = JSON.stringify(this.formatTeamsPayload(event));
4036
+ } else {
4037
+ payload = JSON.stringify(event);
4038
+ }
3822
4039
  const headers = {
3823
4040
  "Content-Type": "application/json",
3824
4041
  "User-Agent": "SchemaShift-Webhook/1.0",
@@ -4134,6 +4351,161 @@ var PluginLoader = class {
4134
4351
  }
4135
4352
  };
4136
4353
 
4354
+ // src/schema-verifier.ts
4355
+ var PRIMITIVE_SAMPLES = {
4356
+ string: [
4357
+ { name: "empty string", input: "", expectedValid: true },
4358
+ { name: "normal string", input: "hello world", expectedValid: true },
4359
+ { name: "number as string", input: "12345", expectedValid: true },
4360
+ { name: "null input", input: null, expectedValid: false },
4361
+ { name: "number input", input: 42, expectedValid: false },
4362
+ { name: "boolean input", input: true, expectedValid: false },
4363
+ { name: "undefined input", input: void 0, expectedValid: false }
4364
+ ],
4365
+ number: [
4366
+ { name: "zero", input: 0, expectedValid: true },
4367
+ { name: "positive int", input: 42, expectedValid: true },
4368
+ { name: "negative int", input: -1, expectedValid: true },
4369
+ { name: "float", input: 3.14, expectedValid: true },
4370
+ { name: "string input", input: "hello", expectedValid: false },
4371
+ { name: "null input", input: null, expectedValid: false },
4372
+ { name: "NaN input", input: Number.NaN, expectedValid: false }
4373
+ ],
4374
+ boolean: [
4375
+ { name: "true", input: true, expectedValid: true },
4376
+ { name: "false", input: false, expectedValid: true },
4377
+ { name: "string input", input: "true", expectedValid: false },
4378
+ { name: "number input", input: 1, expectedValid: false },
4379
+ { name: "null input", input: null, expectedValid: false }
4380
+ ],
4381
+ date: [
4382
+ { name: "valid date", input: /* @__PURE__ */ new Date("2024-01-01"), expectedValid: true },
4383
+ { name: "string input", input: "2024-01-01", expectedValid: false },
4384
+ { name: "null input", input: null, expectedValid: false }
4385
+ ]
4386
+ };
4387
+ var EMAIL_SAMPLES = [
4388
+ { name: "valid email", input: "test@example.com", expectedValid: true },
4389
+ { name: "invalid email", input: "not-an-email", expectedValid: false },
4390
+ { name: "empty string", input: "", expectedValid: false }
4391
+ ];
4392
+ var URL_SAMPLES = [
4393
+ { name: "valid url", input: "https://example.com", expectedValid: true },
4394
+ { name: "invalid url", input: "not a url", expectedValid: false }
4395
+ ];
4396
+ var UUID_SAMPLES = [
4397
+ { name: "valid uuid", input: "550e8400-e29b-41d4-a716-446655440000", expectedValid: true },
4398
+ { name: "invalid uuid", input: "not-a-uuid", expectedValid: false }
4399
+ ];
4400
+ function extractSchemaNames(sourceText) {
4401
+ const schemas = [];
4402
+ const patterns = [
4403
+ /(?:const|let|var)\s+(\w+)\s*=\s*(?:z\.|yup\.|Joi\.|v\.|t\.|S\.|type\(|object\(|string\()/g,
4404
+ /export\s+(?:const|let|var)\s+(\w+)\s*=\s*(?:z\.|yup\.|Joi\.|v\.|t\.|S\.|type\(|object\(|string\()/g
4405
+ ];
4406
+ for (const pattern of patterns) {
4407
+ for (const match of sourceText.matchAll(pattern)) {
4408
+ const name = match[1];
4409
+ if (name && !schemas.includes(name)) {
4410
+ schemas.push(name);
4411
+ }
4412
+ }
4413
+ }
4414
+ return schemas;
4415
+ }
4416
+ function generateSamples(sourceText, schemaName, maxSamples) {
4417
+ const samples = [];
4418
+ const schemaBlock = extractSchemaBlock(sourceText, schemaName);
4419
+ if (!schemaBlock) return PRIMITIVE_SAMPLES.string?.slice(0, maxSamples) ?? [];
4420
+ if (/\.email\s*\(/.test(schemaBlock)) {
4421
+ samples.push(...EMAIL_SAMPLES);
4422
+ }
4423
+ if (/\.url\s*\(/.test(schemaBlock)) {
4424
+ samples.push(...URL_SAMPLES);
4425
+ }
4426
+ if (/\.uuid\s*\(/.test(schemaBlock)) {
4427
+ samples.push(...UUID_SAMPLES);
4428
+ }
4429
+ if (/string\s*\(/.test(schemaBlock)) {
4430
+ samples.push(...PRIMITIVE_SAMPLES.string ?? []);
4431
+ }
4432
+ if (/number\s*\(/.test(schemaBlock) || /\.int\s*\(/.test(schemaBlock)) {
4433
+ samples.push(...PRIMITIVE_SAMPLES.number ?? []);
4434
+ }
4435
+ if (/boolean\s*\(/.test(schemaBlock)) {
4436
+ samples.push(...PRIMITIVE_SAMPLES.boolean ?? []);
4437
+ }
4438
+ if (/date\s*\(/.test(schemaBlock)) {
4439
+ samples.push(...PRIMITIVE_SAMPLES.date ?? []);
4440
+ }
4441
+ if (/\.optional\s*\(/.test(schemaBlock) || /optional\s*\(/.test(schemaBlock)) {
4442
+ samples.push({ name: "undefined (optional)", input: void 0, expectedValid: true });
4443
+ }
4444
+ if (/\.nullable\s*\(/.test(schemaBlock) || /nullable\s*\(/.test(schemaBlock)) {
4445
+ samples.push({ name: "null (nullable)", input: null, expectedValid: true });
4446
+ }
4447
+ if (/\.min\s*\(\s*(\d+)/.test(schemaBlock)) {
4448
+ const minMatch = schemaBlock.match(/\.min\s*\(\s*(\d+)/);
4449
+ const minVal = minMatch ? Number.parseInt(minMatch[1] ?? "0", 10) : 0;
4450
+ samples.push({
4451
+ name: `below min (${minVal})`,
4452
+ input: minVal > 0 ? "a".repeat(minVal - 1) : "",
4453
+ expectedValid: false
4454
+ });
4455
+ }
4456
+ const seen = /* @__PURE__ */ new Set();
4457
+ const unique = [];
4458
+ for (const s of samples) {
4459
+ if (!seen.has(s.name)) {
4460
+ seen.add(s.name);
4461
+ unique.push(s);
4462
+ }
4463
+ }
4464
+ return unique.slice(0, maxSamples);
4465
+ }
4466
+ function extractSchemaBlock(sourceText, schemaName) {
4467
+ const escapedName = schemaName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
4468
+ const pattern = new RegExp(
4469
+ `(?:const|let|var|export\\s+const)\\s+${escapedName}\\s*=\\s*([\\s\\S]*?)(?:;\\s*$|;\\s*(?:const|let|var|export|function|class|type|interface))`,
4470
+ "m"
4471
+ );
4472
+ const match = sourceText.match(pattern);
4473
+ return match?.[1] ?? null;
4474
+ }
4475
+ function createVerificationReport(from, to, results) {
4476
+ const totalSchemas = results.length;
4477
+ const overallParityScore = totalSchemas > 0 ? results.reduce((sum, r) => sum + r.parityScore, 0) / totalSchemas : 100;
4478
+ return {
4479
+ from,
4480
+ to,
4481
+ totalSchemas,
4482
+ results,
4483
+ overallParityScore: Math.round(overallParityScore * 100) / 100,
4484
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
4485
+ };
4486
+ }
4487
+ function formatVerificationReport(report) {
4488
+ const lines = [];
4489
+ lines.push(`
4490
+ Schema Verification Report: ${report.from} \u2192 ${report.to}`);
4491
+ lines.push("\u2500".repeat(50));
4492
+ for (const result of report.results) {
4493
+ const icon = result.parityScore === 100 ? "\u2713" : result.parityScore >= 80 ? "\u26A0" : "\u2717";
4494
+ lines.push(
4495
+ ` ${icon} ${result.schemaName} \u2014 ${result.parityScore}% parity (${result.matchingSamples}/${result.totalSamples} samples)`
4496
+ );
4497
+ for (const mismatch of result.mismatches) {
4498
+ lines.push(
4499
+ ` \u2514\u2500 ${mismatch.sampleName}: source=${mismatch.sourceResult.valid ? "valid" : "invalid"}, target=${mismatch.targetResult.valid ? "valid" : "invalid"}`
4500
+ );
4501
+ }
4502
+ }
4503
+ lines.push("\u2500".repeat(50));
4504
+ lines.push(`Overall Parity: ${report.overallParityScore}%`);
4505
+ lines.push("");
4506
+ return lines.join("\n");
4507
+ }
4508
+
4137
4509
  // src/standard-schema.ts
4138
4510
  import { existsSync as existsSync10, readFileSync as readFileSync10 } from "fs";
4139
4511
  import { join as join10 } from "path";
@@ -4632,10 +5004,14 @@ export {
4632
5004
  buildCallChain,
4633
5005
  computeParallelBatches,
4634
5006
  conditionalValidation,
5007
+ createVerificationReport,
4635
5008
  dependentFields,
4636
5009
  detectFormLibraries,
4637
5010
  detectSchemaLibrary,
4638
5011
  detectStandardSchema,
5012
+ extractSchemaNames,
5013
+ formatVerificationReport,
5014
+ generateSamples,
4639
5015
  getAllMigrationTemplates,
4640
5016
  getGovernanceTemplate,
4641
5017
  getGovernanceTemplateNames,