From c58dbe10f50fcf67ca506f2bfaf573ba05f8b2ef Mon Sep 17 00:00:00 2001 From: Julian Ahlmark Date: Fri, 29 May 2026 15:44:51 +0300 Subject: [PATCH] Add enterprise contract drift guard --- enterprise-contract-drift-guard/demo.js | 48 +++++++ enterprise-contract-drift-guard/index.js | 122 ++++++++++++++++++ enterprise-contract-drift-guard/readme.md | 28 ++++ .../render-video.js | 38 ++++++ .../reports/contract-drift-report.json | 115 +++++++++++++++++ .../reports/contract-drift-report.md | 11 ++ .../reports/demo.mp4 | Bin 0 -> 7070 bytes .../reports/summary.svg | 9 ++ .../sample-data.js | 103 +++++++++++++++ enterprise-contract-drift-guard/test.js | 27 ++++ 10 files changed, 501 insertions(+) create mode 100644 enterprise-contract-drift-guard/demo.js create mode 100644 enterprise-contract-drift-guard/index.js create mode 100644 enterprise-contract-drift-guard/readme.md create mode 100644 enterprise-contract-drift-guard/render-video.js create mode 100644 enterprise-contract-drift-guard/reports/contract-drift-report.json create mode 100644 enterprise-contract-drift-guard/reports/contract-drift-report.md create mode 100644 enterprise-contract-drift-guard/reports/demo.mp4 create mode 100644 enterprise-contract-drift-guard/reports/summary.svg create mode 100644 enterprise-contract-drift-guard/sample-data.js create mode 100644 enterprise-contract-drift-guard/test.js 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 0000000000000000000000000000000000000000..f479e0699ff1e5c4598cb923a415de9dac28a4ab GIT binary patch literal 7070 zcmeHKZ&Vc56@M!Th*ls$gA{d$#GWcFvkPnxGa@PxQBPGW+9v5bJ2S8gGdsh~z%FW1 z)QFlU<|IZ^^#rVGG(LPHSl;W5)?j7gAEO$db@6GX8LA;b%1b{6^p zE$AzRo?MR%2><%q_XfazWIVlJ^KsM=ghD%2nJxz5i)yik88krAYYlK8ZI9qQ4maos zcp0ZnOyF2m0Vp^<{aSpYb zW*S&D-g6a=DTIDc>-4UuSFR!ygi(SJ84+WuGN2H9$F;_%k38t^#*?FMs_fm=v0rM| z5Qpug{GG4q7z$fe!*V%*R$wG5xwOod<`a$-=#T-xv?^9L*N=0lB^(f&J0< z0f$E*KK^_85r+30l2OaYILK`VGoX2M8|OWy4R=AdT$AW8;0NCYaxpwcc#`3Xg=Ydh z_%9pNf${(Uemwoic?}%He?23D95;x8ushJ(N?DbL{+9x$^;i}^u%JQ_mT5`CHSb|d za_Ck2nOarE2FJ3rHW>#Qg349^mCXb{5?92q0-1x7{6xI~S`e9aAwxZ$-EGJXhu z2EouanxRYt6n!d%U^yEvGR2fBgUlca+DY@}3P+Ldlnl~sGNlj#N7*?yp_JNydX|us za+nHltISd+BMFn>MFcccL=@19YL%2?hlDkB10|AGEGSSol}a)bs$GT|U?`2?MX{K+ z!^qmlG!qK%U|=zQ43col8VKVMXcaHS;Z!-9r$Gt$HojD*%dLzkNVE!L2KERpgbpW| z07aR`It4NbbdSB;0$OlQ)_#ULO<&qO8=3lr z8gm$l-mZwB9-ceCXT*f~ocOxq6W)ytN~;~0Jgo79pPl+}MaHxnp|9;~iow$>(sM4S z+{vkb@pRFTmcS)92<3gD9L#wOl5Fqo^9J_H*_aJQ!OeROC5h^qMboZH#_Kd$3Pn$Pma%FvF>4wP5s%u==FZSn;{&RS^CFUQoLikSC7uMc2 z2X=hq_-pg^cgn&>M56Oeg)5iDxr)`#d-qD2*Aw@ycyDC%=IA#X|J}Cye#YYS^=qxo zTbBRn(8h1Cwq4qeh8Y@i7MyE1ri>c8>@WLww2gExG2WaVu`^;+%Q-3cIz6doapw^v z9lbDo|GtvLCqsW5vT$1Mp_+Ryt!Q3-^yQTcCta?afBPi=`H9emtIvKkg^l{SBDO9% z?d02Bq`|uBK+mk7m+ws5`1Mb>-_CB#o&D<{@7Vu|`8`7g3d>zL)aAMUuR7BZVtSRZ zeoXL*4GT`>zu&n^X{oDRcW+HkOYZT5J+*6MLSIO4Yq~M(sr?-lDE$6Y&3DEg>_KSR zR3dinvayS=#71_-&3R(=>DbQ0&p-d0`4+KV*>*ECAXn;r`I9TSBRT9~ypfuGsXTU(_C$(YW+=ODY>m=)I3eXH?(=C%BF+=4C74Zr-= zr3Dv8uCOPJC_7Wx+Hv&K4cDz3Pg*L_!P>mSJ6nXjL~o3RybeYuWmV5`uZ7P9K$+I* z?w|_q-`&2-dTbfyjK6wu=TfhB_!iKfY^bKy84Vh1>88k88&_}2c=;@$s|1k`ilFlA zA$+a^)JGetnW8@R-QTon!iYH_8K1S)KIg+M`<8iOE>E9F`b>D|Wr0deR&`}|RAREe zw#>IicY#UxVrkb6f6%{<-~omKa@Sx;{K*86djOL60XZF10P=M9D4&q+gCVhDfb8%I z+2sdP_p)ZBZ^)XY!H{_SMjL99eL-$Z^#iGUS+mFwWXWJi-OC!qC#2gCq}R(TKad9o zLwddZbTFhIKHEO?1L^hhq94fngCV_Me&-YNiXTXC#E$a~SxxzZtj_cWxi!HLr0!+) z3_p+qlC>{Myb-uD7L)bE-ebr0*ULgrZ*}DNAcw5h5r5EYEFikP tx2+|f-gKx{55Gr((6B=~;%2yPP1e6imL4LTG@am;AcGtECy%+s{RarglOX^A literal 0 HcmV?d00001 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");