From 78db685c7c70459e525cd90ae8eaa6ce7a2ce9c1 Mon Sep 17 00:00:00 2001 From: Ethan Miller Date: Fri, 22 May 2026 02:47:42 -0400 Subject: [PATCH] Add repository license compatibility guard --- .../README.md | 33 ++ .../acceptance-notes.md | 19 + .../demo.js | 31 ++ .../index.js | 326 ++++++++++++++++++ .../package.json | 13 + .../reports/demo.mp4 | Bin 0 -> 13261 bytes .../reports/reviewer-packet.md | 87 +++++ .../reports/summary.json | 131 +++++++ .../reports/summary.svg | 29 ++ .../requirements-map.md | 19 + .../sample-data.js | 92 +++++ .../test.js | 43 +++ 12 files changed, 823 insertions(+) create mode 100644 repository-license-compatibility-guard/README.md create mode 100644 repository-license-compatibility-guard/acceptance-notes.md create mode 100644 repository-license-compatibility-guard/demo.js create mode 100644 repository-license-compatibility-guard/index.js create mode 100644 repository-license-compatibility-guard/package.json create mode 100644 repository-license-compatibility-guard/reports/demo.mp4 create mode 100644 repository-license-compatibility-guard/reports/reviewer-packet.md create mode 100644 repository-license-compatibility-guard/reports/summary.json create mode 100644 repository-license-compatibility-guard/reports/summary.svg create mode 100644 repository-license-compatibility-guard/requirements-map.md create mode 100644 repository-license-compatibility-guard/sample-data.js create mode 100644 repository-license-compatibility-guard/test.js diff --git a/repository-license-compatibility-guard/README.md b/repository-license-compatibility-guard/README.md new file mode 100644 index 00000000..9b3bc5e1 --- /dev/null +++ b/repository-license-compatibility-guard/README.md @@ -0,0 +1,33 @@ +# Repository License Compatibility Guard + +This module provides a focused Project Repository & Version Control slice for +SCIBASE issue #10. It evaluates a synthetic repository release/export manifest +before DOI publication or partner export, checking code package licenses, dataset +licenses, model weight terms, generated figure reuse, fork attribution, SPDX +normalization, missing notices, and release-blocking conflicts. + +## What It Covers + +- License compatibility for tagged repository releases. +- Dataset and model export holds for proprietary, missing, or unsupported terms. +- Non-commercial license conflicts for commercial/partner export intents. +- Copyleft source-bundle and fork-license evidence checks. +- Attribution and notice completion tasks before publication. +- Deterministic reviewer packets and audit digests. + +## Run + +```bash +npm run check +npm test +npm run demo +``` + +Generated artifacts are written to `reports/`: + +- `summary.json` +- `reviewer-packet.md` +- `summary.svg` +- `demo.mp4` + +The data is synthetic and does not scan a live repository or legal system. diff --git a/repository-license-compatibility-guard/acceptance-notes.md b/repository-license-compatibility-guard/acceptance-notes.md new file mode 100644 index 00000000..4e2367e2 --- /dev/null +++ b/repository-license-compatibility-guard/acceptance-notes.md @@ -0,0 +1,19 @@ +# Acceptance Notes + +The guard classifies release assets as: + +- `publishable`: compatible with the requested release/export intent. +- `notice_required`: export can proceed after attribution or notice completion. +- `conflict`: license terms conflict with the export intent. +- `hold`: release-blocking missing, proprietary, unsupported, or incomplete evidence. + +Validation commands: + +```bash +npm run check +npm test +npm run demo +ffprobe -v error -show_entries format=duration,size -show_entries stream=codec_name,width,height,pix_fmt -of default=noprint_wrappers=1 reports/demo.mp4 +git diff --check +git diff --cached --check +``` diff --git a/repository-license-compatibility-guard/demo.js b/repository-license-compatibility-guard/demo.js new file mode 100644 index 00000000..5586cda4 --- /dev/null +++ b/repository-license-compatibility-guard/demo.js @@ -0,0 +1,31 @@ +const fs = require("node:fs"); +const path = require("node:path"); +const { + analyzeRepositoryLicenses, + renderMarkdownReport, + renderSvgSummary, +} = require("./index"); +const { sampleRepositoryLicensePacket } = require("./sample-data"); + +const reportsDir = path.join(__dirname, "reports"); +fs.mkdirSync(reportsDir, { recursive: true }); + +const result = analyzeRepositoryLicenses(sampleRepositoryLicensePacket, { + asOf: "2026-05-22T12:00:00.000Z", +}); + +fs.writeFileSync( + path.join(reportsDir, "summary.json"), + `${JSON.stringify(result, null, 2)}\n` +); +fs.writeFileSync( + path.join(reportsDir, "reviewer-packet.md"), + renderMarkdownReport(result) +); +fs.writeFileSync( + path.join(reportsDir, "summary.svg"), + renderSvgSummary(result) +); + +console.log("repository license compatibility guard demo artifacts written"); +console.log(`audit digest: ${result.auditDigest}`); diff --git a/repository-license-compatibility-guard/index.js b/repository-license-compatibility-guard/index.js new file mode 100644 index 00000000..4203d17c --- /dev/null +++ b/repository-license-compatibility-guard/index.js @@ -0,0 +1,326 @@ +const crypto = require("node:crypto"); + +const PUBLISHABLE = "publishable"; +const NOTICE_REQUIRED = "notice_required"; +const CONFLICT = "conflict"; +const HOLD = "hold"; + +const PERMISSIVE_LICENSES = new Set(["MIT", "Apache-2.0", "BSD-2-Clause", "BSD-3-Clause"]); +const OPEN_DATA_LICENSES = new Set(["CC-BY-4.0", "CC0-1.0", "ODC-BY-1.0"]); +const COPYLEFT_LICENSES = new Set(["GPL-3.0", "AGPL-3.0", "LGPL-3.0"]); +const NON_COMMERCIAL_LICENSES = new Set(["CC-BY-NC-4.0", "CC-BY-NC-SA-4.0"]); + +function requireFields(value, fields, label) { + for (const field of fields) { + if (value[field] === undefined || value[field] === null || value[field] === "") { + throw new Error(`missing required ${label} field: ${field}`); + } + } +} + +function normalizeLicense(value) { + return String(value || "").trim(); +} + +function stableStringify(value) { + if (Array.isArray(value)) { + return `[${value.map(stableStringify).join(",")}]`; + } + + if (value && typeof value === "object") { + return `{${Object.keys(value) + .sort() + .map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`) + .join(",")}}`; + } + + return JSON.stringify(value); +} + +function createAuditDigest(result) { + const payload = { + asOf: result.asOf, + repositoryId: result.repositoryId, + versionTag: result.versionTag, + totals: result.totals, + decisions: result.decisions.map((decision) => ({ + id: decision.id, + license: decision.license, + status: decision.status, + reasons: decision.reasons, + })), + }; + + return crypto.createHash("sha256").update(stableStringify(payload)).digest("hex"); +} + +function requiresNotice(license) { + return ( + PERMISSIVE_LICENSES.has(license) || + OPEN_DATA_LICENSES.has(license) || + COPYLEFT_LICENSES.has(license) || + NON_COMMERCIAL_LICENSES.has(license) + ); +} + +function isCommercialExport(intent) { + return /commercial|partner|enterprise/i.test(intent); +} + +function evaluateAsset(asset, release) { + requireFields(asset, ["id", "componentType", "path", "exportIncluded"], "asset"); + + const license = normalizeLicense(asset.license); + const reasons = []; + const actions = []; + const base = { + id: asset.id, + componentType: asset.componentType, + path: asset.path, + license, + exportIncluded: Boolean(asset.exportIncluded), + status: PUBLISHABLE, + releaseBlocking: false, + reasons, + actions, + }; + + if (!asset.exportIncluded) { + return { + ...base, + status: PUBLISHABLE, + reasons: ["asset is not included in the release export"], + actions: ["No release action required"], + }; + } + + if (!license) { + return { + ...base, + status: HOLD, + releaseBlocking: true, + reasons: ["asset has no normalized SPDX or data license"], + actions: [ + "Hold tagged release and export bundle", + "Attach license metadata or remove asset from export", + ], + }; + } + + if (/proprietary|restricted|unknown/i.test(license)) { + return { + ...base, + status: HOLD, + releaseBlocking: true, + reasons: ["asset license is proprietary, restricted, or unknown"], + actions: [ + "Exclude from public DOI/export package", + "Request rights review and replacement artifact", + ], + }; + } + + if (NON_COMMERCIAL_LICENSES.has(license) && isCommercialExport(release.exportIntent)) { + return { + ...base, + status: CONFLICT, + releaseBlocking: true, + reasons: ["non-commercial license conflicts with commercial partner export intent"], + actions: [ + "Block commercial export until asset is relicensed or omitted", + "Generate public-only export variant if permitted", + ], + }; + } + + if (COPYLEFT_LICENSES.has(license)) { + const sourceOkay = release.sourceBundleIncluded && asset.sourceAvailable; + const forkOkay = !asset.derivedFromFork || asset.forkLicense === license; + if (!sourceOkay || !forkOkay) { + return { + ...base, + status: HOLD, + releaseBlocking: true, + reasons: [ + !sourceOkay + ? "copyleft asset requires source bundle availability" + : "fork attribution license does not match derived asset", + ], + actions: [ + "Hold release until source and fork license evidence are complete", + "Attach copy of license and source bundle manifest", + ], + }; + } + } + + if (requiresNotice(license) && (!asset.noticeIncluded || !asset.attribution)) { + return { + ...base, + status: NOTICE_REQUIRED, + releaseBlocking: false, + reasons: ["license permits export but attribution or notice is incomplete"], + actions: [ + "Add license notice to export manifest", + "Include attribution in citation and repository notice files", + ], + }; + } + + if ( + !PERMISSIVE_LICENSES.has(license) && + !OPEN_DATA_LICENSES.has(license) && + !COPYLEFT_LICENSES.has(license) && + !NON_COMMERCIAL_LICENSES.has(license) + ) { + return { + ...base, + status: HOLD, + releaseBlocking: true, + reasons: ["license is not in the supported compatibility matrix"], + actions: [ + "Route to legal/repository steward review before release", + "Map license to compatibility policy", + ], + }; + } + + return { + ...base, + status: PUBLISHABLE, + releaseBlocking: false, + reasons: ["license is compatible with the requested release/export intent"], + actions: [ + "Allow asset in DOI/export package", + "Preserve license metadata in manifest", + ], + }; +} + +function analyzeRepositoryLicenses(packet, options = {}) { + requireFields(packet, ["release", "assets"], "repository license packet"); + requireFields(packet.release, ["repositoryId", "versionTag", "exportIntent"], "release"); + + if (!Array.isArray(packet.assets)) { + throw new Error("assets must be an array"); + } + + const asOf = options.asOf || packet.asOf || new Date().toISOString(); + const decisions = packet.assets.map((asset) => evaluateAsset(asset, packet.release)); + const totals = decisions.reduce( + (acc, decision) => { + acc.totalAssets += 1; + acc.releaseBlockingAssets += decision.releaseBlocking ? 1 : 0; + acc.byStatus[decision.status] = (acc.byStatus[decision.status] || 0) + 1; + return acc; + }, + { + totalAssets: 0, + releaseBlockingAssets: 0, + byStatus: {}, + } + ); + const result = { + asOf, + repositoryId: packet.release.repositoryId, + versionTag: packet.release.versionTag, + exportIntent: packet.release.exportIntent, + totals, + decisions, + }; + + return { + ...result, + auditDigest: createAuditDigest(result), + }; +} + +function renderMarkdownReport(result) { + const lines = [ + "# Repository License Compatibility Guard", + "", + `Repository: ${result.repositoryId}`, + `Version: ${result.versionTag}`, + `Export intent: ${result.exportIntent}`, + `Audit digest: \`${result.auditDigest}\``, + "", + "## Totals", + "", + `- Assets evaluated: ${result.totals.totalAssets}`, + `- Release-blocking assets: ${result.totals.releaseBlockingAssets}`, + ]; + + for (const [status, count] of Object.entries(result.totals.byStatus).sort()) { + lines.push(`- ${status}: ${count}`); + } + + lines.push("", "## Asset Decisions", ""); + for (const decision of result.decisions) { + lines.push( + `### ${decision.id}`, + "", + `- Path: ${decision.path}`, + `- Type: ${decision.componentType}`, + `- License: ${decision.license || "(missing)"}`, + `- Status: ${decision.status}`, + `- Release blocking: ${decision.releaseBlocking}`, + `- Reasons: ${decision.reasons.join("; ")}`, + `- Actions: ${decision.actions.join("; ")}`, + "" + ); + } + + return `${lines.join("\n").trimEnd()}\n`; +} + +function escapeXml(value) { + return String(value) + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """); +} + +function renderSvgSummary(result) { + const rows = Object.entries(result.totals.byStatus) + .sort() + .map(([status, count], index) => { + const y = 154 + index * 46; + const width = Math.max(44, count * 94); + return `${escapeXml(status)} + + ${count}`; + }) + .join("\n "); + + return ` + + + + Repository License Compatibility Guard + Assets ${result.totals.totalAssets} | Blocking ${result.totals.releaseBlockingAssets} + ${rows} + audit ${escapeXml(result.auditDigest.slice(0, 48))} + +`; +} + +module.exports = { + PUBLISHABLE, + NOTICE_REQUIRED, + CONFLICT, + HOLD, + analyzeRepositoryLicenses, + createAuditDigest, + renderMarkdownReport, + renderSvgSummary, +}; diff --git a/repository-license-compatibility-guard/package.json b/repository-license-compatibility-guard/package.json new file mode 100644 index 00000000..14ef6b50 --- /dev/null +++ b/repository-license-compatibility-guard/package.json @@ -0,0 +1,13 @@ +{ + "name": "repository-license-compatibility-guard", + "version": "1.0.0", + "private": true, + "description": "Synthetic repository dependency license compatibility guard for SCIBASE issue #10.", + "main": "index.js", + "scripts": { + "check": "node --check index.js && node --check sample-data.js && node --check demo.js && node --check test.js", + "test": "node test.js", + "demo": "node demo.js" + }, + "license": "MIT" +} diff --git a/repository-license-compatibility-guard/reports/demo.mp4 b/repository-license-compatibility-guard/reports/demo.mp4 new file mode 100644 index 0000000000000000000000000000000000000000..a8678b2e7ea2b873e8522ba21cfd7950a530a06d GIT binary patch literal 13261 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 + + + + Repository License Compatibility Guard + Assets 7 | Blocking 3 + conflict + + 1 + hold + + 2 + notice_required + + 2 + publishable + + 2 + audit 734ff57751d2a28105e23bf2507f2da485edf49f470f9048 + diff --git a/repository-license-compatibility-guard/requirements-map.md b/repository-license-compatibility-guard/requirements-map.md new file mode 100644 index 00000000..4e639578 --- /dev/null +++ b/repository-license-compatibility-guard/requirements-map.md @@ -0,0 +1,19 @@ +# Requirements Map + +| Issue #10 area | Coverage | +| --- | --- | +| Repository structure and components | Evaluates code, dataset, notebook, model, and figure assets in a release manifest. | +| File and metadata versioning | Binds decisions to a repository ID and semantic version tag. | +| Collaboration and forking | Checks fork-derived asset license continuity and attribution. | +| Computation-aware reproducibility | Holds model/notebook assets when source or license evidence is incomplete. | +| Repository identifiers and citation | Blocks DOI/export publication when license and attribution evidence is unsafe. | +| Programmatic access and export | Produces deterministic decision packets for export-bundle gating. | + +## Non-overlap + +This is not a broad repository ledger, release engine, structured diff/rollback, +provenance attestation, release embargo, notebook replay, schema migration, +citation impact, API/export, merge queue, environment drift, access review, +DOI tombstone, metadata readiness, branch lineage, or sensitive-artifact commit +slice. It focuses on dependency, dataset, model, and generated-artifact license +compatibility before release/export. diff --git a/repository-license-compatibility-guard/sample-data.js b/repository-license-compatibility-guard/sample-data.js new file mode 100644 index 00000000..b18010ab --- /dev/null +++ b/repository-license-compatibility-guard/sample-data.js @@ -0,0 +1,92 @@ +const sampleRepositoryLicensePacket = { + asOf: "2026-05-22T12:00:00.000Z", + release: { + repositoryId: "repo-neuro-open-atlas", + versionTag: "v1.4.0", + exportIntent: "public_doi_and_commercial_partner_export", + forkedFrom: "repo-vision-source", + sourceBundleIncluded: true, + }, + assets: [ + { + id: "asset-code-core", + componentType: "code", + path: "code/atlas_pipeline.py", + license: "MIT", + attribution: "SCIBASE Neuro Team", + noticeIncluded: true, + sourceAvailable: true, + derivedFromFork: false, + exportIncluded: true, + }, + { + id: "asset-dataset-images", + componentType: "dataset", + path: "data/images-manifest.json", + license: "CC-BY-4.0", + attribution: "Open Imaging Consortium", + noticeIncluded: false, + sourceAvailable: true, + derivedFromFork: false, + exportIncluded: true, + }, + { + id: "asset-training-table", + componentType: "dataset", + path: "data/training-table.csv", + license: "CC-BY-NC-4.0", + attribution: "Partner Lab", + noticeIncluded: true, + sourceAvailable: true, + derivedFromFork: false, + exportIncluded: true, + }, + { + id: "asset-model-weights", + componentType: "model", + path: "models/segmentation-weights.bin", + license: "Proprietary", + attribution: "Private Vendor", + noticeIncluded: true, + sourceAvailable: false, + derivedFromFork: false, + exportIncluded: true, + }, + { + id: "asset-notebook", + componentType: "notebook", + path: "notebooks/analysis.ipynb", + license: "GPL-3.0", + attribution: "Legacy Methods Group", + noticeIncluded: false, + sourceAvailable: true, + derivedFromFork: true, + forkLicense: "GPL-3.0", + exportIncluded: true, + }, + { + id: "asset-figure", + componentType: "figure", + path: "results/figure-3.svg", + license: "CC0-1.0", + attribution: "Generated from synthetic data", + noticeIncluded: true, + sourceAvailable: true, + derivedFromFork: false, + exportIncluded: true, + }, + { + id: "asset-raw-supplement", + componentType: "dataset", + path: "data/supplement-private.tsv", + license: "", + attribution: "", + noticeIncluded: false, + sourceAvailable: false, + derivedFromFork: false, + exportIncluded: true, + }, + ], +}; + +module.exports = { sampleRepositoryLicensePacket }; diff --git a/repository-license-compatibility-guard/test.js b/repository-license-compatibility-guard/test.js new file mode 100644 index 00000000..33066a26 --- /dev/null +++ b/repository-license-compatibility-guard/test.js @@ -0,0 +1,43 @@ +const assert = require("node:assert/strict"); +const { + CONFLICT, + HOLD, + NOTICE_REQUIRED, + PUBLISHABLE, + analyzeRepositoryLicenses, + createAuditDigest, + renderMarkdownReport, + renderSvgSummary, +} = require("./index"); +const { sampleRepositoryLicensePacket } = require("./sample-data"); + +const result = analyzeRepositoryLicenses(sampleRepositoryLicensePacket); + +assert.equal(result.totals.totalAssets, 7); +assert.equal(result.totals.releaseBlockingAssets, 3); +assert.equal(result.totals.byStatus[PUBLISHABLE], 2); +assert.equal(result.totals.byStatus[NOTICE_REQUIRED], 2); +assert.equal(result.totals.byStatus[CONFLICT], 1); +assert.equal(result.totals.byStatus[HOLD], 2); + +const commercialConflict = result.decisions.find((decision) => decision.id === "asset-training-table"); +assert.equal(commercialConflict.status, CONFLICT); +assert.match(commercialConflict.reasons.join(" "), /non-commercial license/); + +const missingLicense = result.decisions.find((decision) => decision.id === "asset-raw-supplement"); +assert.equal(missingLicense.status, HOLD); +assert.equal(missingLicense.releaseBlocking, true); + +const gplNotebook = result.decisions.find((decision) => decision.id === "asset-notebook"); +assert.equal(gplNotebook.status, NOTICE_REQUIRED); + +assert.equal(createAuditDigest(result), result.auditDigest); +assert.match(renderMarkdownReport(result), /Repository License Compatibility Guard/); +assert.match(renderSvgSummary(result), /Blocking 3/); + +assert.throws( + () => analyzeRepositoryLicenses({ release: {}, assets: [] }), + /missing required release field: repositoryId/ +); + +console.log("repository license compatibility guard tests passed");