diff --git a/enterprise-contract-drift-guard/demo.js b/enterprise-contract-drift-guard/demo.js new file mode 100644 index 00000000..73b485dd --- /dev/null +++ b/enterprise-contract-drift-guard/demo.js @@ -0,0 +1,48 @@ +const fs = require("fs"); +const path = require("path"); +const { contracts } = require("./sample-data"); +const { evaluateContracts } = require("./index"); + +const outDir = path.join(__dirname, "reports"); +fs.mkdirSync(outDir, { recursive: true }); + +const report = evaluateContracts(contracts); +fs.writeFileSync(path.join(outDir, "contract-drift-report.json"), JSON.stringify(report, null, 2)); + +const rows = report.reports + .map((entry) => `| ${entry.system} | ${entry.decision} | ${entry.highestSeverity} | ${entry.findings.length} |`) + .join("\n"); + +fs.writeFileSync( + path.join(outDir, "contract-drift-report.md"), + [ + "# Enterprise Contract Drift Guard", + "", + "| System | Decision | Highest severity | Findings |", + "| --- | --- | --- | ---: |", + rows, + "", + `Release holds: ${report.releaseHolds}`, + `Approvals: ${report.approvals}` + ].join("\n") +); + +fs.writeFileSync( + path.join(outDir, "summary.svg"), + ` + + Enterprise Contract Drift Guard + Synthetic institutional integration contract review + + ${report.releaseHolds} release holds + + ${report.approvals} approvals +` +); + +console.log(JSON.stringify({ + totalContracts: report.totalContracts, + releaseHolds: report.releaseHolds, + approvals: report.approvals, + findings: report.reports.reduce((sum, entry) => sum + entry.findings.length, 0) +}, null, 2)); diff --git a/enterprise-contract-drift-guard/index.js b/enterprise-contract-drift-guard/index.js new file mode 100644 index 00000000..9d40a12c --- /dev/null +++ b/enterprise-contract-drift-guard/index.js @@ -0,0 +1,122 @@ +function classifyChange(beforeType, afterType) { + if (beforeType === afterType) return "compatible"; + if (!afterType) return "removed-field"; + if (!beforeType) return "added-field"; + if (beforeType.startsWith("enum:") && afterType.startsWith("enum:")) { + const beforeValues = beforeType.slice(5).split("|"); + const afterValues = afterType.slice(5).split("|"); + return beforeValues.every((value) => afterValues.includes(value)) + ? "enum-expanded" + : "enum-narrowed"; + } + return "type-changed"; +} + +function evaluateContract(contract) { + const fields = new Set([ + ...Object.keys(contract.currentSchema), + ...Object.keys(contract.proposedSchema) + ]); + const findings = []; + + for (const field of fields) { + const change = classifyChange(contract.currentSchema[field], contract.proposedSchema[field]); + if (change === "compatible" || change === "added-field") continue; + findings.push({ + field, + type: change, + current: contract.currentSchema[field] || null, + proposed: contract.proposedSchema[field] || null, + severity: change === "removed-field" || change === "type-changed" ? "high" : "medium" + }); + } + + for (const field of contract.requiredFields) { + if (!contract.proposedSchema[field]) { + findings.push({ + field, + type: "missing-required-field", + severity: "critical", + current: contract.currentSchema[field] || null, + proposed: null + }); + } + } + + if (contract.deprecationDays < 90) { + findings.push({ + field: "deprecationDays", + type: "short-deprecation-window", + severity: "medium", + current: contract.deprecationDays, + proposed: ">=90" + }); + } + + if (!contract.payloadPolicy.redactsEmails) { + findings.push({ + field: "payloadPolicy.redactsEmails", + type: "pii-redaction-disabled", + severity: "high", + current: false, + proposed: true + }); + } + + if (contract.payloadPolicy.includesInternalNotes) { + findings.push({ + field: "payloadPolicy.includesInternalNotes", + type: "internal-notes-leak-risk", + severity: "high", + current: true, + proposed: false + }); + } + + const highest = findings.some((finding) => finding.severity === "critical") + ? "critical" + : findings.some((finding) => finding.severity === "high") + ? "high" + : findings.some((finding) => finding.severity === "medium") + ? "medium" + : "clear"; + + return { + id: contract.id, + system: contract.system, + owner: contract.owner, + currentVersion: contract.currentVersion, + proposedVersion: contract.proposedVersion, + decision: highest === "clear" ? "approve" : highest === "medium" ? "stage-with-notice" : "hold-release", + highestSeverity: highest, + findings, + remediation: findings.map((finding) => remediationFor(finding, contract)) + }; +} + +function remediationFor(finding, contract) { + const actions = { + "removed-field": `Restore ${finding.field} or publish a compatibility alias for ${contract.consumerPinnedVersion}.`, + "missing-required-field": `Keep ${finding.field} in the proposed ${contract.system} payload until all consumers migrate.`, + "type-changed": `Version-gate ${finding.field} and keep the old type available for pinned consumers.`, + "enum-narrowed": `Do not remove enum values without a documented migration and replay fixture.`, + "short-deprecation-window": "Extend the deprecation period to at least 90 days for enterprise integrations.", + "pii-redaction-disabled": "Enable email redaction before exporting review or roster payloads.", + "internal-notes-leak-risk": "Strip internal notes from outbound institutional payloads." + }; + return actions[finding.type] || `Review ${finding.field} before release.`; +} + +function evaluateContracts(contracts) { + const reports = contracts.map(evaluateContract); + return { + generatedAt: "2026-05-29T00:00:00.000Z", + totalContracts: reports.length, + releaseHolds: reports.filter((report) => report.decision === "hold-release").length, + stagedNotices: reports.filter((report) => report.decision === "stage-with-notice").length, + approvals: reports.filter((report) => report.decision === "approve").length, + reports + }; +} + +module.exports = { classifyChange, evaluateContract, evaluateContracts }; diff --git a/enterprise-contract-drift-guard/readme.md b/enterprise-contract-drift-guard/readme.md new file mode 100644 index 00000000..81c5129b --- /dev/null +++ b/enterprise-contract-drift-guard/readme.md @@ -0,0 +1,28 @@ +# Enterprise Contract Drift Guard + +This module is a focused Enterprise Tooling slice for institutional integrations. It reviews proposed API, webhook, and export contract changes before they reach external systems such as DSpace, Invenio, Canvas, Moodle, ELNs, lab inventory tools, HRIS, and ORCID sync jobs. + +It is intentionally separate from broad enterprise dashboards, webhook delivery, webhook replay, webhook payload redaction, SCIM deprovisioning, repository sync SLA checks, LMS roster passback, connector certification, vendor DPA review, data residency, and admin alert escalation work. + +## What it checks + +- Removed required fields in export and webhook payloads +- Type changes that would break pinned enterprise consumers +- Narrowed enum values +- Deprecation windows below 90 days +- Email redaction and internal-note leakage policy +- Versioned remediation actions for institutional integration owners + +## Run + +```bash +node enterprise-contract-drift-guard/test.js +node enterprise-contract-drift-guard/demo.js +node enterprise-contract-drift-guard/render-video.js +``` + +Outputs are written to `enterprise-contract-drift-guard/reports/`. + +## Safety + +All data is synthetic. The module does not call institutional repositories, LMS platforms, ELNs, HRIS tools, ORCID, webhooks, private APIs, credentials, or production SCIBASE services. diff --git a/enterprise-contract-drift-guard/render-video.js b/enterprise-contract-drift-guard/render-video.js new file mode 100644 index 00000000..53bd6209 --- /dev/null +++ b/enterprise-contract-drift-guard/render-video.js @@ -0,0 +1,38 @@ +const { execFileSync } = require("child_process"); +const fs = require("fs"); +const path = require("path"); + +const outDir = path.join(__dirname, "reports"); +fs.mkdirSync(outDir, { recursive: true }); + +const ppm = path.join(outDir, "demo-frame.ppm"); +const mp4 = path.join(outDir, "demo.mp4"); +const width = 960; +const height = 540; +const pixels = []; + +for (let y = 0; y < height; y += 1) { + for (let x = 0; x < width; x += 1) { + const inPanel = x > 80 && x < 880 && y > 90 && y < 450; + const bar = y > 210 && y < 260 && x > 150 && x < 650; + const okBar = y > 310 && y < 360 && x > 150 && x < 310; + const r = inPanel ? (bar ? 214 : okBar ? 53 : 26) : 9; + const g = inPanel ? (bar ? 69 : okBar ? 166 : 40) : 14; + const b = inPanel ? (bar ? 69 : okBar ? 111 : 53) : 22; + pixels.push(String.fromCharCode(r, g, b)); + } +} + +fs.writeFileSync(ppm, `P6\n${width} ${height}\n255\n` + pixels.join(""), "binary"); +execFileSync("ffmpeg", [ + "-y", + "-loop", "1", + "-framerate", "24", + "-i", ppm, + "-t", "5", + "-vf", "format=yuv420p", + "-movflags", "+faststart", + mp4 +], { stdio: "inherit" }); + +console.log(mp4); diff --git a/enterprise-contract-drift-guard/reports/contract-drift-report.json b/enterprise-contract-drift-guard/reports/contract-drift-report.json new file mode 100644 index 00000000..50083a96 --- /dev/null +++ b/enterprise-contract-drift-guard/reports/contract-drift-report.json @@ -0,0 +1,115 @@ +{ + "generatedAt": "2026-05-29T00:00:00.000Z", + "totalContracts": 4, + "releaseHolds": 3, + "stagedNotices": 0, + "approvals": 1, + "reports": [ + { + "id": "dspace-repository-export", + "system": "DSpace", + "owner": "University Library", + "currentVersion": "2026.05", + "proposedVersion": "2026.06", + "decision": "hold-release", + "highestSeverity": "critical", + "findings": [ + { + "field": "license", + "type": "type-changed", + "current": "string", + "proposed": "enum:cc-by|cc0", + "severity": "high" + }, + { + "field": "embargoUntil", + "type": "removed-field", + "current": "date", + "proposed": null, + "severity": "high" + }, + { + "field": "embargoUntil", + "type": "missing-required-field", + "severity": "critical", + "current": "date", + "proposed": null + } + ], + "remediation": [ + "Version-gate license and keep the old type available for pinned consumers.", + "Restore embargoUntil or publish a compatibility alias for 2026.05.", + "Keep embargoUntil in the proposed DSpace payload until all consumers migrate." + ] + }, + { + "id": "canvas-course-publication-webhook", + "system": "Canvas", + "owner": "Graduate School", + "currentVersion": "2026.04", + "proposedVersion": "2026.06", + "decision": "hold-release", + "highestSeverity": "high", + "findings": [ + { + "field": "courseId", + "type": "type-changed", + "current": "string", + "proposed": "number", + "severity": "high" + }, + { + "field": "eventType", + "type": "enum-narrowed", + "current": "enum:created|published|reviewed", + "proposed": "enum:created|published|retracted", + "severity": "medium" + }, + { + "field": "deprecationDays", + "type": "short-deprecation-window", + "severity": "medium", + "current": 30, + "proposed": ">=90" + } + ], + "remediation": [ + "Version-gate courseId and keep the old type available for pinned consumers.", + "Do not remove enum values without a documented migration and replay fixture.", + "Extend the deprecation period to at least 90 days for enterprise integrations." + ] + }, + { + "id": "orcid-hris-identity-sync", + "system": "ORCID-HRIS", + "owner": "Research HR", + "currentVersion": "2026.03", + "proposedVersion": "2026.06", + "decision": "approve", + "highestSeverity": "clear", + "findings": [], + "remediation": [] + }, + { + "id": "eln-inventory-review-export", + "system": "ELN", + "owner": "Lab Operations", + "currentVersion": "2026.05", + "proposedVersion": "2026.06", + "decision": "hold-release", + "highestSeverity": "high", + "findings": [ + { + "field": "payloadPolicy.redactsEmails", + "type": "pii-redaction-disabled", + "severity": "high", + "current": false, + "proposed": true + } + ], + "remediation": [ + "Enable email redaction before exporting review or roster payloads." + ] + } + ] +} \ No newline at end of file diff --git a/enterprise-contract-drift-guard/reports/contract-drift-report.md b/enterprise-contract-drift-guard/reports/contract-drift-report.md new file mode 100644 index 00000000..4e259e0e --- /dev/null +++ b/enterprise-contract-drift-guard/reports/contract-drift-report.md @@ -0,0 +1,11 @@ +# Enterprise Contract Drift Guard + +| System | Decision | Highest severity | Findings | +| --- | --- | --- | ---: | +| DSpace | hold-release | critical | 3 | +| Canvas | hold-release | high | 3 | +| ORCID-HRIS | approve | clear | 0 | +| ELN | hold-release | high | 1 | + +Release holds: 3 +Approvals: 1 \ No newline at end of file diff --git a/enterprise-contract-drift-guard/reports/demo.mp4 b/enterprise-contract-drift-guard/reports/demo.mp4 new file mode 100644 index 00000000..f479e069 Binary files /dev/null and b/enterprise-contract-drift-guard/reports/demo.mp4 differ diff --git a/enterprise-contract-drift-guard/reports/summary.svg b/enterprise-contract-drift-guard/reports/summary.svg new file mode 100644 index 00000000..9e42636f --- /dev/null +++ b/enterprise-contract-drift-guard/reports/summary.svg @@ -0,0 +1,9 @@ + + + Enterprise Contract Drift Guard + Synthetic institutional integration contract review + + 3 release holds + + 1 approvals + \ No newline at end of file diff --git a/enterprise-contract-drift-guard/sample-data.js b/enterprise-contract-drift-guard/sample-data.js new file mode 100644 index 00000000..22f2a7dc --- /dev/null +++ b/enterprise-contract-drift-guard/sample-data.js @@ -0,0 +1,103 @@ +const contracts = [ + { + id: "dspace-repository-export", + owner: "University Library", + system: "DSpace", + currentVersion: "2026.05", + proposedVersion: "2026.06", + deprecationDays: 120, + requiredFields: ["doi", "title", "authors", "license", "embargoUntil"], + currentSchema: { + doi: "string", + title: "string", + authors: "array", + license: "string", + embargoUntil: "date", + reproducibilityScore: "number" + }, + proposedSchema: { + doi: "string", + title: "string", + authors: "array", + license: "enum:cc-by|cc0", + reproducibilityScore: "number" + }, + payloadPolicy: { redactsEmails: true, includesInternalNotes: false }, + consumerPinnedVersion: "2026.05" + }, + { + id: "canvas-course-publication-webhook", + owner: "Graduate School", + system: "Canvas", + currentVersion: "2026.04", + proposedVersion: "2026.06", + deprecationDays: 30, + requiredFields: ["projectId", "courseId", "eventType", "publishedAt"], + currentSchema: { + projectId: "string", + courseId: "string", + eventType: "enum:created|published|reviewed", + publishedAt: "date" + }, + proposedSchema: { + projectId: "string", + courseId: "number", + eventType: "enum:created|published|retracted", + publishedAt: "date" + }, + payloadPolicy: { redactsEmails: true, includesInternalNotes: false }, + consumerPinnedVersion: "2026.04" + }, + { + id: "orcid-hris-identity-sync", + owner: "Research HR", + system: "ORCID-HRIS", + currentVersion: "2026.03", + proposedVersion: "2026.06", + deprecationDays: 180, + requiredFields: ["orcid", "institutionUserId", "displayName", "affiliation"], + currentSchema: { + orcid: "string", + institutionUserId: "string", + displayName: "string", + affiliation: "string", + departmentCode: "string" + }, + proposedSchema: { + orcid: "string", + institutionUserId: "string", + displayName: "string", + affiliation: "string", + departmentCode: "string", + role: "enum:faculty|staff|student" + }, + payloadPolicy: { redactsEmails: true, includesInternalNotes: false }, + consumerPinnedVersion: "2026.03" + }, + { + id: "eln-inventory-review-export", + owner: "Lab Operations", + system: "ELN", + currentVersion: "2026.05", + proposedVersion: "2026.06", + deprecationDays: 90, + requiredFields: ["experimentId", "inventoryIds", "reviewScore", "reviewedAt"], + currentSchema: { + experimentId: "string", + inventoryIds: "array", + reviewScore: "number", + reviewedAt: "date" + }, + proposedSchema: { + experimentId: "string", + inventoryIds: "array", + reviewScore: "number", + reviewedAt: "date", + reviewerEmail: "string" + }, + payloadPolicy: { redactsEmails: false, includesInternalNotes: false }, + consumerPinnedVersion: "2026.05" + } +]; + +module.exports = { contracts }; diff --git a/enterprise-contract-drift-guard/test.js b/enterprise-contract-drift-guard/test.js new file mode 100644 index 00000000..2bc1b636 --- /dev/null +++ b/enterprise-contract-drift-guard/test.js @@ -0,0 +1,27 @@ +const assert = require("assert"); +const { contracts } = require("./sample-data"); +const { classifyChange, evaluateContracts } = require("./index"); + +assert.strictEqual(classifyChange("string", "string"), "compatible"); +assert.strictEqual(classifyChange("string", "number"), "type-changed"); +assert.strictEqual(classifyChange("string", undefined), "removed-field"); +assert.strictEqual(classifyChange("enum:a|b", "enum:a|b|c"), "enum-expanded"); +assert.strictEqual(classifyChange("enum:a|b", "enum:a"), "enum-narrowed"); + +const report = evaluateContracts(contracts); +assert.strictEqual(report.totalContracts, 4); +assert.strictEqual(report.releaseHolds, 3); +assert.strictEqual(report.stagedNotices, 0); +assert.strictEqual(report.approvals, 1); + +const dspace = report.reports.find((entry) => entry.id === "dspace-repository-export"); +assert(dspace.findings.some((finding) => finding.type === "missing-required-field")); + +const canvas = report.reports.find((entry) => entry.id === "canvas-course-publication-webhook"); +assert(canvas.findings.some((finding) => finding.type === "type-changed")); +assert(canvas.findings.some((finding) => finding.type === "short-deprecation-window")); + +const eln = report.reports.find((entry) => entry.id === "eln-inventory-review-export"); +assert(eln.findings.some((finding) => finding.type === "pii-redaction-disabled")); + +console.log("enterprise-contract-drift-guard tests passed");