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 00000000..a8678b2e Binary files /dev/null and b/repository-license-compatibility-guard/reports/demo.mp4 differ diff --git a/repository-license-compatibility-guard/reports/reviewer-packet.md b/repository-license-compatibility-guard/reports/reviewer-packet.md new file mode 100644 index 00000000..d6792416 --- /dev/null +++ b/repository-license-compatibility-guard/reports/reviewer-packet.md @@ -0,0 +1,87 @@ +# Repository License Compatibility Guard + +Repository: repo-neuro-open-atlas +Version: v1.4.0 +Export intent: public_doi_and_commercial_partner_export +Audit digest: `734ff57751d2a28105e23bf2507f2da485edf49f470f9048cb1e19b332b9d71b` + +## Totals + +- Assets evaluated: 7 +- Release-blocking assets: 3 +- conflict: 1 +- hold: 2 +- notice_required: 2 +- publishable: 2 + +## Asset Decisions + +### asset-code-core + +- Path: code/atlas_pipeline.py +- Type: code +- License: MIT +- Status: publishable +- Release blocking: false +- Reasons: license is compatible with the requested release/export intent +- Actions: Allow asset in DOI/export package; Preserve license metadata in manifest + +### asset-dataset-images + +- Path: data/images-manifest.json +- Type: dataset +- License: CC-BY-4.0 +- Status: notice_required +- Release blocking: 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 + +### asset-training-table + +- Path: data/training-table.csv +- Type: dataset +- License: CC-BY-NC-4.0 +- Status: conflict +- Release blocking: 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 + +### asset-model-weights + +- Path: models/segmentation-weights.bin +- Type: model +- License: Proprietary +- Status: hold +- Release blocking: true +- Reasons: asset license is proprietary, restricted, or unknown +- Actions: Exclude from public DOI/export package; Request rights review and replacement artifact + +### asset-notebook + +- Path: notebooks/analysis.ipynb +- Type: notebook +- License: GPL-3.0 +- Status: notice_required +- Release blocking: 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 + +### asset-figure + +- Path: results/figure-3.svg +- Type: figure +- License: CC0-1.0 +- Status: publishable +- Release blocking: false +- Reasons: license is compatible with the requested release/export intent +- Actions: Allow asset in DOI/export package; Preserve license metadata in manifest + +### asset-raw-supplement + +- Path: data/supplement-private.tsv +- Type: dataset +- License: (missing) +- Status: hold +- Release blocking: 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 diff --git a/repository-license-compatibility-guard/reports/summary.json b/repository-license-compatibility-guard/reports/summary.json new file mode 100644 index 00000000..fc5d0acb --- /dev/null +++ b/repository-license-compatibility-guard/reports/summary.json @@ -0,0 +1,131 @@ +{ + "asOf": "2026-05-22T12:00:00.000Z", + "repositoryId": "repo-neuro-open-atlas", + "versionTag": "v1.4.0", + "exportIntent": "public_doi_and_commercial_partner_export", + "totals": { + "totalAssets": 7, + "releaseBlockingAssets": 3, + "byStatus": { + "publishable": 2, + "notice_required": 2, + "conflict": 1, + "hold": 2 + } + }, + "decisions": [ + { + "id": "asset-code-core", + "componentType": "code", + "path": "code/atlas_pipeline.py", + "license": "MIT", + "exportIncluded": true, + "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" + ] + }, + { + "id": "asset-dataset-images", + "componentType": "dataset", + "path": "data/images-manifest.json", + "license": "CC-BY-4.0", + "exportIncluded": true, + "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" + ] + }, + { + "id": "asset-training-table", + "componentType": "dataset", + "path": "data/training-table.csv", + "license": "CC-BY-NC-4.0", + "exportIncluded": true, + "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" + ] + }, + { + "id": "asset-model-weights", + "componentType": "model", + "path": "models/segmentation-weights.bin", + "license": "Proprietary", + "exportIncluded": true, + "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" + ] + }, + { + "id": "asset-notebook", + "componentType": "notebook", + "path": "notebooks/analysis.ipynb", + "license": "GPL-3.0", + "exportIncluded": true, + "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" + ] + }, + { + "id": "asset-figure", + "componentType": "figure", + "path": "results/figure-3.svg", + "license": "CC0-1.0", + "exportIncluded": true, + "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" + ] + }, + { + "id": "asset-raw-supplement", + "componentType": "dataset", + "path": "data/supplement-private.tsv", + "license": "", + "exportIncluded": true, + "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" + ] + } + ], + "auditDigest": "734ff57751d2a28105e23bf2507f2da485edf49f470f9048cb1e19b332b9d71b" +} diff --git a/repository-license-compatibility-guard/reports/summary.svg b/repository-license-compatibility-guard/reports/summary.svg new file mode 100644 index 00000000..6f396553 --- /dev/null +++ b/repository-license-compatibility-guard/reports/summary.svg @@ -0,0 +1,29 @@ + + + + + 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");