Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions embargoed-data-license-guard/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Embargoed Data License Guard

This module is a focused Revenue Infrastructure slice for issue #20. It checks
whether an anonymized research metadata or data-licensing export can be released
to an institutional, government, or market-intelligence customer.

It is intentionally self-contained:

- no payment processor calls
- no external APIs
- no credentials
- no private research content
- synthetic packets only

## What It Checks

- Embargo windows have ended or the license has a valid early-access grant.
- Exclusive licenses are not released to the wrong customer.
- Cohort size is large enough for anonymized export.
- Consent coverage meets the license policy.
- License tier permits the requested export type.
- Required attribution, redaction, and data-processing terms are present.
- Deliverables are not stale relative to the license snapshot.

## Run

```bash
npm test
npm run demo
```

The demo writes reviewer artifacts under `artifacts/`:

- `demo-output.json`
- `demo-report.md`
- `release-summary.svg`

## Requirement Mapping

Issue #20 calls out monetizing structured, anonymized usage and research
metadata through licensing APIs and analytics. This guard focuses on the release
control before such a licensing export is delivered or invoiced.

| Issue requirement | Covered by |
| --- | --- |
| Data licensing models for institutional and government customers | customer/license packet fields |
| Anonymized metadata, never private content | cohort and redaction checks |
| API/dashboard licensing options | `exportType` tier entitlement checks |
| Predictable revenue and compliance-safe delivery | deterministic release decisions |
68 changes: 68 additions & 0 deletions embargoed-data-license-guard/artifacts/demo-output.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
[
{
"licenseId": "lic-gov-neuro-2026",
"customer": "NIH Policy Lab",
"exportType": "api",
"decision": "RELEASE",
"findings": [],
"financeAction": "release_export_and_invoice"
},
{
"licenseId": "lic-market-rare-disease-2026",
"customer": "Market Intel A",
"exportType": "periodic_snapshot",
"decision": "REVIEW",
"findings": [
{
"code": "EARLY_ACCESS_REVIEW",
"severity": "REVIEW",
"message": "Export is still under embargo but has an early-access approval."
},
{
"code": "CONSENT_GAP",
"severity": "REVIEW",
"message": "Consent coverage 92% is below required 95%."
}
],
"financeAction": "route_to_licensing_operations"
},
{
"licenseId": "lic-exclusive-funder-2026",
"customer": "Regional Consortium",
"exportType": "white_label",
"decision": "HOLD",
"findings": [
{
"code": "EXCLUSIVITY_CONFLICT",
"severity": "HOLD",
"message": "License conflicts with an active exclusive customer grant."
},
{
"code": "COHORT_TOO_SMALL",
"severity": "HOLD",
"message": "Cohort size 42 is below minimum 100."
},
{
"code": "CONSENT_GAP",
"severity": "REVIEW",
"message": "Consent coverage 88% is below required 95%."
},
{
"code": "TIER_NOT_ENTITLED",
"severity": "HOLD",
"message": "Dashboard license does not permit white_label exports."
},
{
"code": "MISSING_RELEASE_TERMS",
"severity": "REVIEW",
"message": "Missing data processing agreement, redaction profile, attribution text."
},
{
"code": "STALE_SNAPSHOT",
"severity": "REVIEW",
"message": "Snapshot is 57 days old; maximum is 30."
}
],
"financeAction": "hold_export_and_block_invoice"
}
]
25 changes: 25 additions & 0 deletions embargoed-data-license-guard/artifacts/demo-report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Embargoed Data License Guard Demo

| License | Customer | Export | Decision | Finance action |
| --- | --- | --- | --- | --- |
| lic-gov-neuro-2026 | NIH Policy Lab | api | RELEASE | release_export_and_invoice |
| lic-market-rare-disease-2026 | Market Intel A | periodic_snapshot | REVIEW | route_to_licensing_operations |
| lic-exclusive-funder-2026 | Regional Consortium | white_label | HOLD | hold_export_and_block_invoice |

### lic-gov-neuro-2026

- RELEASE: no blocking findings

### lic-market-rare-disease-2026

- REVIEW: EARLY_ACCESS_REVIEW - Export is still under embargo but has an early-access approval.
- REVIEW: CONSENT_GAP - Consent coverage 92% is below required 95%.

### lic-exclusive-funder-2026

- HOLD: EXCLUSIVITY_CONFLICT - License conflicts with an active exclusive customer grant.
- HOLD: COHORT_TOO_SMALL - Cohort size 42 is below minimum 100.
- REVIEW: CONSENT_GAP - Consent coverage 88% is below required 95%.
- HOLD: TIER_NOT_ENTITLED - Dashboard license does not permit white_label exports.
- REVIEW: MISSING_RELEASE_TERMS - Missing data processing agreement, redaction profile, attribution text.
- REVIEW: STALE_SNAPSHOT - Snapshot is 57 days old; maximum is 30.
6 changes: 6 additions & 0 deletions embargoed-data-license-guard/artifacts/release-summary.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
11 changes: 11 additions & 0 deletions embargoed-data-license-guard/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"name": "embargoed-data-license-guard",
"version": "1.0.0",
"private": true,
"description": "Dependency-free release guard for embargoed research data licensing exports.",
"type": "module",
"scripts": {
"demo": "node src/cli.js",
"test": "node test/license-guard.test.js"
}
}
17 changes: 17 additions & 0 deletions embargoed-data-license-guard/src/cli.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { evaluateLicenseRelease } from "./licenseGuard.js";
import { writeArtifacts } from "./report.js";
import { samplePackets } from "./samplePackets.js";

const results = samplePackets.map((packet) => evaluateLicenseRelease(packet));

for (const result of results) {
console.log(
`${result.licenseId}: ${result.decision} (${result.financeAction})`,
);
for (const finding of result.findings) {
console.log(` - ${finding.severity} ${finding.code}: ${finding.message}`);
}
}

writeArtifacts(results);
console.log("Artifacts written to artifacts/");
164 changes: 164 additions & 0 deletions embargoed-data-license-guard/src/licenseGuard.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
const EXPORT_TIER_ORDER = {
dashboard: 1,
periodic_snapshot: 2,
api: 3,
white_label: 4,
};

const DECISION_PRIORITY = {
RELEASE: 0,
REVIEW: 1,
HOLD: 2,
};

export function evaluateLicenseRelease(packet, today = "2026-06-27") {
const findings = [
checkEmbargo(packet, today),
checkExclusivity(packet),
checkCohortSize(packet),
checkConsentCoverage(packet),
checkTier(packet),
checkTerms(packet),
checkSnapshotFreshness(packet, today),
].filter(Boolean);

const decision = findings.reduce(
(current, finding) =>
DECISION_PRIORITY[finding.severity] > DECISION_PRIORITY[current]
? finding.severity
: current,
"RELEASE",
);

return {
licenseId: packet.licenseId,
customer: packet.customer.name,
exportType: packet.export.type,
decision,
findings,
financeAction: financeActionFor(decision),
};
}

export function summarizeResults(results) {
return {
total: results.length,
release: results.filter((result) => result.decision === "RELEASE").length,
review: results.filter((result) => result.decision === "REVIEW").length,
hold: results.filter((result) => result.decision === "HOLD").length,
};
}

function checkEmbargo(packet, today) {
if (packet.export.embargoEndsAt <= today) {
return null;
}
if (packet.license.allowEarlyAccess && packet.license.earlyAccessApprovalId) {
return {
code: "EARLY_ACCESS_REVIEW",
severity: "REVIEW",
message: "Export is still under embargo but has an early-access approval.",
};
}
return {
code: "EMBARGO_ACTIVE",
severity: "HOLD",
message: `Export is embargoed until ${packet.export.embargoEndsAt}.`,
};
}

function checkExclusivity(packet) {
const exclusive = packet.license.exclusiveCustomerId;
if (!exclusive || exclusive === packet.customer.id) {
return null;
}
return {
code: "EXCLUSIVITY_CONFLICT",
severity: "HOLD",
message: "License conflicts with an active exclusive customer grant.",
};
}

function checkCohortSize(packet) {
if (packet.export.cohortSize >= packet.policy.minimumCohortSize) {
return null;
}
return {
code: "COHORT_TOO_SMALL",
severity: "HOLD",
message: `Cohort size ${packet.export.cohortSize} is below minimum ${packet.policy.minimumCohortSize}.`,
};
}

function checkConsentCoverage(packet) {
if (packet.export.consentCoverage >= packet.policy.minimumConsentCoverage) {
return null;
}
return {
code: "CONSENT_GAP",
severity: "REVIEW",
message: `Consent coverage ${packet.export.consentCoverage}% is below required ${packet.policy.minimumConsentCoverage}%.`,
};
}

function checkTier(packet) {
const requested = EXPORT_TIER_ORDER[packet.export.type] ?? Number.POSITIVE_INFINITY;
const allowed = EXPORT_TIER_ORDER[packet.license.maxExportType] ?? 0;
if (requested <= allowed) {
return null;
}
return {
code: "TIER_NOT_ENTITLED",
severity: "HOLD",
message: `${packet.license.tier} license does not permit ${packet.export.type} exports.`,
};
}

function checkTerms(packet) {
const missing = [];
if (!packet.license.hasDataProcessingAgreement) {
missing.push("data processing agreement");
}
if (!packet.export.redactionProfileId) {
missing.push("redaction profile");
}
if (!packet.export.attributionText) {
missing.push("attribution text");
}
if (missing.length === 0) {
return null;
}
return {
code: "MISSING_RELEASE_TERMS",
severity: "REVIEW",
message: `Missing ${missing.join(", ")}.`,
};
}

function checkSnapshotFreshness(packet, today) {
const snapshotAgeDays = daysBetween(packet.export.snapshotAt, today);
if (snapshotAgeDays <= packet.policy.maximumSnapshotAgeDays) {
return null;
}
return {
code: "STALE_SNAPSHOT",
severity: "REVIEW",
message: `Snapshot is ${snapshotAgeDays} days old; maximum is ${packet.policy.maximumSnapshotAgeDays}.`,
};
}

function daysBetween(from, to) {
const start = Date.parse(`${from}T00:00:00Z`);
const end = Date.parse(`${to}T00:00:00Z`);
return Math.floor((end - start) / 86_400_000);
}

function financeActionFor(decision) {
if (decision === "RELEASE") {
return "release_export_and_invoice";
}
if (decision === "REVIEW") {
return "route_to_licensing_operations";
}
return "hold_export_and_block_invoice";
}
Loading