diff --git a/enterprise-style-package-provenance-guard/README.md b/enterprise-style-package-provenance-guard/README.md new file mode 100644 index 00000000..a51cb97d --- /dev/null +++ b/enterprise-style-package-provenance-guard/README.md @@ -0,0 +1,18 @@ +# Enterprise Style Package Provenance Guard + +This module adds a focused Enterprise Tooling export-pipeline guard for journal and funder formatting plugins. It validates synthetic style packages before institution-scale JATS, DOCX, or LaTeX exports are released. + +The guard checks approved plugin versions, required export-format coverage, template checksum parity, citation-style parity, DOI/ORCID/version-history preservation, validation recency, reviewer signoff, generated output provenance digests, and private field leakage in style previews. + +## Commands + +```bash +npm run check +npm test +npm run demo +npm run verify-video +``` + +`npm run demo` writes reviewer artifacts to `reports/`, including JSON packets, a Markdown report, an SVG summary, and a short H.264 MP4 demo. + +Synthetic data only. No live repositories, journal systems, funder portals, credentials, private manuscripts, external APIs, payment systems, or payout-account settings are used. diff --git a/enterprise-style-package-provenance-guard/demo.js b/enterprise-style-package-provenance-guard/demo.js new file mode 100644 index 00000000..487a114e --- /dev/null +++ b/enterprise-style-package-provenance-guard/demo.js @@ -0,0 +1,50 @@ +const fs = require("node:fs"); +const path = require("node:path"); +const { evaluateStylePackage, renderMarkdownReport, renderSvgSummary } = require("./index"); +const { cleanPacket, riskyPacket } = require("./sample-data"); + +const reportsDir = path.join(__dirname, "reports"); +fs.mkdirSync(reportsDir, { recursive: true }); + +const cleanEvaluation = evaluateStylePackage(cleanPacket); +const riskyEvaluation = evaluateStylePackage(riskyPacket); + +fs.writeFileSync( + path.join(reportsDir, "clean-style-package.json"), + `${JSON.stringify({ input: cleanPacket, evaluation: cleanEvaluation }, null, 2)}\n` +); +fs.writeFileSync( + path.join(reportsDir, "risky-style-package.json"), + `${JSON.stringify({ input: riskyPacket, evaluation: riskyEvaluation }, null, 2)}\n` +); +fs.writeFileSync( + path.join(reportsDir, "style-package-provenance-report.md"), + renderMarkdownReport(riskyPacket, riskyEvaluation) +); +fs.writeFileSync( + path.join(reportsDir, "summary.svg"), + renderSvgSummary(riskyEvaluation) +); +fs.writeFileSync( + path.join(reportsDir, "demo-script.txt"), + [ + "Enterprise style package provenance guard demo", + "", + `Clean packet decision: ${cleanEvaluation.summary.decision}`, + `Clean audit digest: ${cleanEvaluation.summary.auditDigest}`, + "", + `Risky packet decision: ${riskyEvaluation.summary.decision}`, + `Risky finding count: ${riskyEvaluation.summary.findingCount}`, + `Risky audit digest: ${riskyEvaluation.summary.auditDigest}`, + "", + "The risky packet demonstrates a formatting plugin drift before JATS/DOCX/LaTeX export: unapproved plugin version, missing formats, checksum drift, citation mismatch, stale validation, missing signoff, metadata preservation gaps, and private preview-field leakage.", + "" + ].join("\n") +); + +console.log(JSON.stringify({ + cleanDecision: cleanEvaluation.summary.decision, + riskyDecision: riskyEvaluation.summary.decision, + riskyFindings: riskyEvaluation.summary.findingCount, + report: "reports/style-package-provenance-report.md" +}, null, 2)); diff --git a/enterprise-style-package-provenance-guard/index.js b/enterprise-style-package-provenance-guard/index.js new file mode 100644 index 00000000..13905813 --- /dev/null +++ b/enterprise-style-package-provenance-guard/index.js @@ -0,0 +1,356 @@ +const crypto = require("node:crypto"); + +function asArray(value) { + return Array.isArray(value) ? value : []; +} + +function stableJson(value) { + if (Array.isArray(value)) { + return `[${value.map(stableJson).join(",")}]`; + } + if (value && typeof value === "object") { + return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableJson(value[key])}`).join(",")}}`; + } + return JSON.stringify(value); +} + +function sha256(value) { + return crypto.createHash("sha256").update(stableJson(value)).digest("hex"); +} + +function toDate(value) { + const parsed = new Date(value || ""); + return Number.isNaN(parsed.getTime()) ? null : parsed; +} + +function daysBetween(laterValue, earlierValue) { + const later = toDate(laterValue); + const earlier = toDate(earlierValue); + if (!later || !earlier) { + return null; + } + return Math.floor((later.getTime() - earlier.getTime()) / (24 * 60 * 60 * 1000)); +} + +function severityRank(severity) { + return { critical: 4, high: 3, medium: 2, low: 1 }[severity] || 0; +} + +function addFinding(findings, severity, code, message, refs, action) { + findings.push({ + severity, + code, + message, + refs: asArray(refs), + action + }); +} + +function isSemver(value) { + return /^\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$/.test(String(value || "")); +} + +function normalizedSet(values) { + return new Set(asArray(values).map((value) => String(value).trim().toLowerCase()).filter(Boolean)); +} + +function setDifference(required, actual) { + const actualSet = normalizedSet(actual); + return asArray(required).filter((value) => !actualSet.has(String(value).trim().toLowerCase())); +} + +function evaluateStylePackage(packet) { + const findings = []; + const reviewDate = packet.reviewDate || new Date().toISOString().slice(0, 10); + const stylePackages = asArray(packet.stylePackages); + const approvedPlugins = new Map(asArray(packet.approvedPlugins).map((plugin) => [plugin.id, plugin])); + const requiredFormats = asArray(packet.requiredExportFormats); + const packageSummaries = []; + + if (stylePackages.length === 0) { + addFinding( + findings, + "critical", + "STYLE_PACKAGE_EMPTY", + "No enterprise formatting style packages were supplied for export review.", + [packet.institutionId || "institution"], + "attach_style_packages_before_export_release" + ); + } + + for (const stylePackage of stylePackages) { + const refs = [stylePackage.id || "style-package"]; + const plugin = approvedPlugins.get(stylePackage.pluginId); + const pluginVersion = String(stylePackage.pluginVersion || ""); + const target = stylePackage.target || {}; + + if (!plugin) { + addFinding( + findings, + "critical", + "PLUGIN_NOT_APPROVED", + `${stylePackage.id || "style package"} uses plugin ${stylePackage.pluginId || "unknown"} without enterprise approval.`, + refs, + "route_plugin_for_enterprise_approval" + ); + } + + if (!isSemver(pluginVersion)) { + addFinding( + findings, + "high", + "PLUGIN_VERSION_NOT_PINNED", + `${stylePackage.id || "style package"} is missing a pinned semantic plugin version.`, + refs, + "pin_plugin_version_before_export" + ); + } else if (plugin && !asArray(plugin.allowedVersions).includes(pluginVersion)) { + addFinding( + findings, + "high", + "PLUGIN_VERSION_NOT_APPROVED", + `${stylePackage.id || "style package"} uses ${pluginVersion}, which is not in the approved plugin version list.`, + refs, + "promote_or_replace_approved_plugin_version" + ); + } + + const missingFormats = setDifference(requiredFormats, stylePackage.formats); + if (missingFormats.length > 0) { + addFinding( + findings, + "high", + "EXPORT_FORMAT_COVERAGE_GAP", + `${stylePackage.id || "style package"} is missing required export formats: ${missingFormats.join(", ")}.`, + refs, + "add_required_export_format_templates" + ); + } + + const templateChecks = asArray(stylePackage.templates); + if (templateChecks.length === 0) { + addFinding( + findings, + "high", + "TEMPLATE_MANIFEST_MISSING", + `${stylePackage.id || "style package"} has no template checksum manifest.`, + refs, + "attach_template_checksum_manifest" + ); + } + + for (const template of templateChecks) { + if (!template.expectedChecksum || !template.actualChecksum) { + addFinding( + findings, + "medium", + "TEMPLATE_CHECKSUM_INCOMPLETE", + `${template.path || stylePackage.id || "template"} lacks both expected and actual checksums.`, + refs, + "record_expected_and_actual_template_checksums" + ); + } else if (template.expectedChecksum !== template.actualChecksum) { + addFinding( + findings, + "critical", + "TEMPLATE_CHECKSUM_DRIFT", + `${template.path || stylePackage.id || "template"} checksum differs from the approved style package manifest.`, + refs, + "block_export_until_template_is_rebuilt_from_approved_source" + ); + } + } + + if (String(stylePackage.citationStyle || "").toLowerCase() !== String(target.citationStyle || "").toLowerCase()) { + addFinding( + findings, + "high", + "CITATION_STYLE_MISMATCH", + `${stylePackage.id || "style package"} uses ${stylePackage.citationStyle || "unknown"} citations for ${target.name || "target"} which requires ${target.citationStyle || "unknown"}.`, + refs, + "select_target_approved_citation_style" + ); + } + + const metadata = stylePackage.metadataPreservation || {}; + const missingMetadata = [ + ["DOI", metadata.doi], + ["ORCID", metadata.orcid], + ["version history", metadata.versionHistory], + ["license", metadata.license], + ["funder award", metadata.funderAward] + ].filter(([, present]) => present !== true).map(([label]) => label); + if (missingMetadata.length > 0) { + addFinding( + findings, + "high", + "EXPORT_METADATA_PRESERVATION_GAP", + `${stylePackage.id || "style package"} does not preserve ${missingMetadata.join(", ")} metadata across export formats.`, + refs, + "preserve_required_publication_metadata" + ); + } + + const validationAge = daysBetween(reviewDate, stylePackage.lastValidatedAt); + if (validationAge === null || validationAge > Number(packet.maxValidationAgeDays || 90)) { + addFinding( + findings, + "medium", + "STYLE_VALIDATION_STALE", + `${stylePackage.id || "style package"} style validation is ${validationAge === null ? "missing" : `${validationAge} days old`}.`, + refs, + "rerun_style_export_validation" + ); + } + + const signoff = stylePackage.reviewerSignoff || {}; + if (!signoff.reviewer || signoff.status !== "approved") { + addFinding( + findings, + "high", + "STYLE_SIGNOFF_MISSING", + `${stylePackage.id || "style package"} lacks approved enterprise reviewer signoff.`, + refs, + "obtain_enterprise_style_reviewer_signoff" + ); + } + + const generatedOutputs = asArray(stylePackage.generatedOutputs); + const missingOutputDigest = generatedOutputs.filter((output) => !output.provenanceDigest || !output.builtFromTemplateChecksum); + if (missingOutputDigest.length > 0) { + addFinding( + findings, + "medium", + "OUTPUT_PROVENANCE_DIGEST_MISSING", + `${stylePackage.id || "style package"} has ${missingOutputDigest.length} generated outputs without build provenance digests.`, + refs, + "record_output_build_provenance_digests" + ); + } + + if (asArray(stylePackage.previewFields).some((field) => field.private === true && field.exported === true)) { + addFinding( + findings, + "critical", + "PRIVATE_FIELD_EXPORTED_IN_STYLE_PREVIEW", + `${stylePackage.id || "style package"} exports private internal style-preview fields.`, + refs, + "remove_private_preview_fields_from_export" + ); + } + + packageSummaries.push({ + id: stylePackage.id, + pluginId: stylePackage.pluginId, + pluginVersion, + target: target.name, + formatCount: asArray(stylePackage.formats).length, + templateCount: templateChecks.length, + validationAgeDays: validationAge, + outputCount: generatedOutputs.length + }); + } + + findings.sort((a, b) => severityRank(b.severity) - severityRank(a.severity) || a.code.localeCompare(b.code)); + const decision = findings.some((finding) => severityRank(finding.severity) >= 4) + ? "hold_enterprise_style_exports" + : findings.some((finding) => severityRank(finding.severity) >= 3) + ? "revise_style_package_before_export" + : findings.some((finding) => finding.severity === "medium") + ? "release_after_style_validation_refresh" + : "release_enterprise_style_exports"; + + const summary = { + institutionId: packet.institutionId, + reviewDate, + decision, + stylePackagesReviewed: stylePackages.length, + findingCount: findings.length, + highOrCriticalFindings: findings.filter((finding) => severityRank(finding.severity) >= 3).length + }; + const auditDigest = `sha256:${sha256({ summary, findings, packageSummaries }).slice(0, 16)}`; + + return { + summary: { + ...summary, + auditDigest + }, + findings, + packageSummaries, + requiredExportFormats: requiredFormats + }; +} + +function renderMarkdownReport(packet, evaluation) { + const lines = [ + `# Enterprise Style Package Provenance Report`, + "", + `Institution: ${packet.institutionId}`, + `Review date: ${evaluation.summary.reviewDate}`, + `Decision: ${evaluation.summary.decision}`, + `Audit digest: ${evaluation.summary.auditDigest}`, + "", + "## Style Packages", + "", + "| Package | Target | Plugin | Formats | Templates | Validation age |", + "| --- | --- | --- | ---: | ---: | ---: |" + ]; + + for (const item of evaluation.packageSummaries) { + lines.push(`| ${item.id} | ${item.target || "unknown"} | ${item.pluginId}@${item.pluginVersion || "unknown"} | ${item.formatCount} | ${item.templateCount} | ${item.validationAgeDays === null ? "missing" : `${item.validationAgeDays}d`} |`); + } + + lines.push("", "## Findings", ""); + if (evaluation.findings.length === 0) { + lines.push("No findings."); + } else { + for (const finding of evaluation.findings) { + lines.push(`- **${finding.severity.toUpperCase()} ${finding.code}**: ${finding.message} Action: \`${finding.action}\`.`); + } + } + + lines.push("", "## Required Export Formats", ""); + for (const format of evaluation.requiredExportFormats) { + lines.push(`- ${format}`); + } + lines.push(""); + return lines.join("\n"); +} + +function renderSvgSummary(evaluation) { + const width = 920; + const height = 420; + const critical = evaluation.findings.filter((finding) => finding.severity === "critical").length; + const high = evaluation.findings.filter((finding) => finding.severity === "high").length; + const medium = evaluation.findings.filter((finding) => finding.severity === "medium").length; + const bars = [ + ["critical", critical, "#b91c1c"], + ["high", high, "#dc2626"], + ["medium", medium, "#d97706"] + ]; + const barSvg = bars.map(([label, count, color], index) => { + const y = 150 + index * 72; + const barWidth = Math.max(16, Math.min(520, count * 54)); + return `${label}${count}`; + }).join(""); + + return [ + ``, + ``, + ``, + `Enterprise Style Package Provenance`, + `Decision: ${evaluation.summary.decision}`, + barSvg, + `Audit ${evaluation.summary.auditDigest}`, + ``, + "" + ].join("\n"); +} + +module.exports = { + evaluateStylePackage, + renderMarkdownReport, + renderSvgSummary, + stableJson, + sha256 +}; diff --git a/enterprise-style-package-provenance-guard/make-demo-video.js b/enterprise-style-package-provenance-guard/make-demo-video.js new file mode 100644 index 00000000..167891f6 --- /dev/null +++ b/enterprise-style-package-provenance-guard/make-demo-video.js @@ -0,0 +1,91 @@ +const fs = require("node:fs"); +const path = require("node:path"); +const { spawnSync } = require("node:child_process"); +const { evaluateStylePackage } = require("./index"); +const { cleanPacket, riskyPacket } = require("./sample-data"); + +const reportsDir = path.join(__dirname, "reports"); +const framesDir = path.join(reportsDir, "frames"); +fs.mkdirSync(framesDir, { recursive: true }); + +const clean = evaluateStylePackage(cleanPacket); +const risky = evaluateStylePackage(riskyPacket); +const width = 960; +const height = 540; +const frames = 72; +const fps = 18; + +function setPixel(buffer, x, y, r, g, b) { + if (x < 0 || y < 0 || x >= width || y >= height) { + return; + } + const offset = (y * width + x) * 3; + buffer[offset] = r; + buffer[offset + 1] = g; + buffer[offset + 2] = b; +} + +function fillRect(buffer, x, y, w, h, r, g, b) { + for (let row = y; row < y + h; row += 1) { + for (let col = x; col < x + w; col += 1) { + setPixel(buffer, col, row, r, g, b); + } + } +} + +function writeFrame(index, progress) { + const buffer = Buffer.alloc(width * height * 3, 250); + fillRect(buffer, 0, 0, width, height, 248, 250, 252); + fillRect(buffer, 56, 48, 848, 444, 255, 255, 255); + fillRect(buffer, 56, 48, 848, 8, 17, 24, 39); + + const cleanWidth = Math.floor(328 * Math.min(1, progress * 1.7)); + const riskyWidth = Math.floor(328 * Math.max(0, (progress - 0.18) * 1.45)); + fillRect(buffer, 104, 116, 328, 64, 226, 232, 240); + fillRect(buffer, 104, 116, cleanWidth, 64, 22, 163, 74); + fillRect(buffer, 528, 116, 328, 64, 226, 232, 240); + fillRect(buffer, 528, 116, riskyWidth, 64, 220, 38, 38); + + for (let i = 0; i < clean.summary.stylePackagesReviewed; i += 1) { + fillRect(buffer, 128 + i * 84, 246, 56, 94, 16, 185, 129); + fillRect(buffer, 136 + i * 84, 256, 40, 18, 255, 255, 255); + fillRect(buffer, 136 + i * 84, 286, 40, 10, 255, 255, 255); + } + + for (let i = 0; i < Math.min(12, risky.summary.findingCount); i += 1) { + const barHeight = 30 + (i % 6) * 18; + fillRect(buffer, 548 + i * 24, 386 - barHeight, 18, barHeight, 185, 28, 28); + } + + fillRect(buffer, 104, 424, Math.floor(744 * progress), 18, 37, 99, 235); + fillRect(buffer, 104, 454, Math.floor(560 * progress), 18, 217, 119, 6); + + const header = Buffer.from(`P6\n${width} ${height}\n255\n`, "ascii"); + fs.writeFileSync(path.join(framesDir, `frame-${String(index).padStart(3, "0")}.ppm`), Buffer.concat([header, buffer])); +} + +for (let index = 0; index < frames; index += 1) { + writeFrame(index, index / (frames - 1)); +} + +const output = path.join(reportsDir, "demo.mp4"); +const result = spawnSync(process.env.FFMPEG_PATH || "ffmpeg", [ + "-y", + "-framerate", + String(fps), + "-i", + path.join(framesDir, "frame-%03d.ppm"), + "-pix_fmt", + "yuv420p", + "-movflags", + "+faststart", + output +], { stdio: "inherit" }); + +fs.rmSync(framesDir, { recursive: true, force: true }); + +if (result.status !== 0) { + process.exit(result.status || 1); +} + +console.log(`Wrote ${output}`); diff --git a/enterprise-style-package-provenance-guard/package.json b/enterprise-style-package-provenance-guard/package.json new file mode 100644 index 00000000..03267324 --- /dev/null +++ b/enterprise-style-package-provenance-guard/package.json @@ -0,0 +1,21 @@ +{ + "name": "enterprise-style-package-provenance-guard", + "version": "1.0.0", + "private": true, + "description": "Synthetic enterprise export formatting-plugin provenance guard for SCIBASE enterprise tooling.", + "main": "index.js", + "scripts": { + "check": "node --check index.js && node --check sample-data.js && node --check test.js && node --check demo.js && node --check make-demo-video.js", + "test": "node test.js", + "demo": "node demo.js && node make-demo-video.js", + "verify-video": "ffprobe -v error -select_streams v:0 -show_entries stream=codec_name,width,height,duration,avg_frame_rate -show_entries format=duration,size -of default=noprint_wrappers=1 reports/demo.mp4" + }, + "keywords": [ + "scibase", + "enterprise-tooling", + "export-pipeline", + "formatting-plugin", + "provenance" + ], + "license": "MIT" +} diff --git a/enterprise-style-package-provenance-guard/reports/clean-style-package.json b/enterprise-style-package-provenance-guard/reports/clean-style-package.json new file mode 100644 index 00000000..82f860d8 --- /dev/null +++ b/enterprise-style-package-provenance-guard/reports/clean-style-package.json @@ -0,0 +1,111 @@ +{ + "input": { + "institutionId": "northstar-research-office", + "reviewDate": "2026-06-01", + "maxValidationAgeDays": 90, + "requiredExportFormats": [ + "JATS", + "DOCX", + "LaTeX" + ], + "approvedPlugins": [ + { + "id": "journal-style-pack", + "allowedVersions": [ + "3.4.2", + "3.4.3" + ], + "owner": "enterprise-publication-ops" + } + ], + "stylePackages": [ + { + "id": "cell-reports-2026-style", + "pluginId": "journal-style-pack", + "pluginVersion": "3.4.3", + "target": { + "name": "Cell Reports", + "citationStyle": "vancouver" + }, + "formats": [ + "JATS", + "DOCX", + "LaTeX" + ], + "citationStyle": "vancouver", + "lastValidatedAt": "2026-05-20", + "reviewerSignoff": { + "reviewer": "pubops-style-reviewer", + "status": "approved" + }, + "templates": [ + { + "path": "templates/cell-reports/article.jats.xml", + "expectedChecksum": "sha256:6bd2f4c0c2a6", + "actualChecksum": "sha256:6bd2f4c0c2a6" + }, + { + "path": "templates/cell-reports/manuscript.docx", + "expectedChecksum": "sha256:a9f28f1de342", + "actualChecksum": "sha256:a9f28f1de342" + } + ], + "metadataPreservation": { + "doi": true, + "orcid": true, + "versionHistory": true, + "license": true, + "funderAward": true + }, + "generatedOutputs": [ + { + "path": "exports/cell-reports/article.xml", + "builtFromTemplateChecksum": "sha256:6bd2f4c0c2a6", + "provenanceDigest": "sha256:output-9122" + }, + { + "path": "exports/cell-reports/manuscript.docx", + "builtFromTemplateChecksum": "sha256:a9f28f1de342", + "provenanceDigest": "sha256:output-8227" + } + ], + "previewFields": [ + { + "name": "publicTitle", + "exported": true, + "private": false + } + ] + } + ] + }, + "evaluation": { + "summary": { + "institutionId": "northstar-research-office", + "reviewDate": "2026-06-01", + "decision": "release_enterprise_style_exports", + "stylePackagesReviewed": 1, + "findingCount": 0, + "highOrCriticalFindings": 0, + "auditDigest": "sha256:3a3a13df45782064" + }, + "findings": [], + "packageSummaries": [ + { + "id": "cell-reports-2026-style", + "pluginId": "journal-style-pack", + "pluginVersion": "3.4.3", + "target": "Cell Reports", + "formatCount": 3, + "templateCount": 2, + "validationAgeDays": 12, + "outputCount": 2 + } + ], + "requiredExportFormats": [ + "JATS", + "DOCX", + "LaTeX" + ] + } +} diff --git a/enterprise-style-package-provenance-guard/reports/demo-script.txt b/enterprise-style-package-provenance-guard/reports/demo-script.txt new file mode 100644 index 00000000..d29b2711 --- /dev/null +++ b/enterprise-style-package-provenance-guard/reports/demo-script.txt @@ -0,0 +1,10 @@ +Enterprise style package provenance guard demo + +Clean packet decision: release_enterprise_style_exports +Clean audit digest: sha256:3a3a13df45782064 + +Risky packet decision: hold_enterprise_style_exports +Risky finding count: 17 +Risky audit digest: sha256:e519c9e6fe7731dd + +The risky packet demonstrates a formatting plugin drift before JATS/DOCX/LaTeX export: unapproved plugin version, missing formats, checksum drift, citation mismatch, stale validation, missing signoff, metadata preservation gaps, and private preview-field leakage. diff --git a/enterprise-style-package-provenance-guard/reports/demo.mp4 b/enterprise-style-package-provenance-guard/reports/demo.mp4 new file mode 100644 index 00000000..c3717323 Binary files /dev/null and b/enterprise-style-package-provenance-guard/reports/demo.mp4 differ diff --git a/enterprise-style-package-provenance-guard/reports/risky-style-package.json b/enterprise-style-package-provenance-guard/reports/risky-style-package.json new file mode 100644 index 00000000..61ee06fc --- /dev/null +++ b/enterprise-style-package-provenance-guard/reports/risky-style-package.json @@ -0,0 +1,292 @@ +{ + "input": { + "institutionId": "northstar-research-office", + "reviewDate": "2026-06-01", + "maxValidationAgeDays": 90, + "requiredExportFormats": [ + "JATS", + "DOCX", + "LaTeX" + ], + "approvedPlugins": [ + { + "id": "journal-style-pack", + "allowedVersions": [ + "3.4.2" + ], + "owner": "enterprise-publication-ops" + } + ], + "stylePackages": [ + { + "id": "horizon-eu-repository-style", + "pluginId": "journal-style-pack", + "pluginVersion": "3.5.0", + "target": { + "name": "Horizon EU grant portal", + "citationStyle": "apa" + }, + "formats": [ + "DOCX" + ], + "citationStyle": "vancouver", + "lastValidatedAt": "2025-12-10", + "reviewerSignoff": { + "reviewer": "", + "status": "draft" + }, + "templates": [ + { + "path": "templates/horizon/report.docx", + "expectedChecksum": "sha256:approved-4411", + "actualChecksum": "sha256:local-drift-7719" + }, + { + "path": "templates/horizon/article.jats.xml", + "expectedChecksum": "", + "actualChecksum": "sha256:missing-expected" + } + ], + "metadataPreservation": { + "doi": true, + "orcid": false, + "versionHistory": false, + "license": true, + "funderAward": false + }, + "generatedOutputs": [ + { + "path": "exports/horizon/report.docx", + "builtFromTemplateChecksum": "", + "provenanceDigest": "" + } + ], + "previewFields": [ + { + "name": "internalReviewerNotes", + "exported": true, + "private": true + } + ] + }, + { + "id": "unapproved-preprint-style", + "pluginId": "community-style-snapshot", + "pluginVersion": "latest", + "target": { + "name": "bioRxiv", + "citationStyle": "vancouver" + }, + "formats": [ + "JATS", + "DOCX" + ], + "citationStyle": "vancouver", + "lastValidatedAt": null, + "reviewerSignoff": null, + "templates": [], + "metadataPreservation": { + "doi": false, + "orcid": false, + "versionHistory": false, + "license": false, + "funderAward": false + }, + "generatedOutputs": [] + } + ] + }, + "evaluation": { + "summary": { + "institutionId": "northstar-research-office", + "reviewDate": "2026-06-01", + "decision": "hold_enterprise_style_exports", + "stylePackagesReviewed": 2, + "findingCount": 17, + "highOrCriticalFindings": 13, + "auditDigest": "sha256:e519c9e6fe7731dd" + }, + "findings": [ + { + "severity": "critical", + "code": "PLUGIN_NOT_APPROVED", + "message": "unapproved-preprint-style uses plugin community-style-snapshot without enterprise approval.", + "refs": [ + "unapproved-preprint-style" + ], + "action": "route_plugin_for_enterprise_approval" + }, + { + "severity": "critical", + "code": "PRIVATE_FIELD_EXPORTED_IN_STYLE_PREVIEW", + "message": "horizon-eu-repository-style exports private internal style-preview fields.", + "refs": [ + "horizon-eu-repository-style" + ], + "action": "remove_private_preview_fields_from_export" + }, + { + "severity": "critical", + "code": "TEMPLATE_CHECKSUM_DRIFT", + "message": "templates/horizon/report.docx checksum differs from the approved style package manifest.", + "refs": [ + "horizon-eu-repository-style" + ], + "action": "block_export_until_template_is_rebuilt_from_approved_source" + }, + { + "severity": "high", + "code": "CITATION_STYLE_MISMATCH", + "message": "horizon-eu-repository-style uses vancouver citations for Horizon EU grant portal which requires apa.", + "refs": [ + "horizon-eu-repository-style" + ], + "action": "select_target_approved_citation_style" + }, + { + "severity": "high", + "code": "EXPORT_FORMAT_COVERAGE_GAP", + "message": "horizon-eu-repository-style is missing required export formats: JATS, LaTeX.", + "refs": [ + "horizon-eu-repository-style" + ], + "action": "add_required_export_format_templates" + }, + { + "severity": "high", + "code": "EXPORT_FORMAT_COVERAGE_GAP", + "message": "unapproved-preprint-style is missing required export formats: LaTeX.", + "refs": [ + "unapproved-preprint-style" + ], + "action": "add_required_export_format_templates" + }, + { + "severity": "high", + "code": "EXPORT_METADATA_PRESERVATION_GAP", + "message": "horizon-eu-repository-style does not preserve ORCID, version history, funder award metadata across export formats.", + "refs": [ + "horizon-eu-repository-style" + ], + "action": "preserve_required_publication_metadata" + }, + { + "severity": "high", + "code": "EXPORT_METADATA_PRESERVATION_GAP", + "message": "unapproved-preprint-style does not preserve DOI, ORCID, version history, license, funder award metadata across export formats.", + "refs": [ + "unapproved-preprint-style" + ], + "action": "preserve_required_publication_metadata" + }, + { + "severity": "high", + "code": "PLUGIN_VERSION_NOT_APPROVED", + "message": "horizon-eu-repository-style uses 3.5.0, which is not in the approved plugin version list.", + "refs": [ + "horizon-eu-repository-style" + ], + "action": "promote_or_replace_approved_plugin_version" + }, + { + "severity": "high", + "code": "PLUGIN_VERSION_NOT_PINNED", + "message": "unapproved-preprint-style is missing a pinned semantic plugin version.", + "refs": [ + "unapproved-preprint-style" + ], + "action": "pin_plugin_version_before_export" + }, + { + "severity": "high", + "code": "STYLE_SIGNOFF_MISSING", + "message": "horizon-eu-repository-style lacks approved enterprise reviewer signoff.", + "refs": [ + "horizon-eu-repository-style" + ], + "action": "obtain_enterprise_style_reviewer_signoff" + }, + { + "severity": "high", + "code": "STYLE_SIGNOFF_MISSING", + "message": "unapproved-preprint-style lacks approved enterprise reviewer signoff.", + "refs": [ + "unapproved-preprint-style" + ], + "action": "obtain_enterprise_style_reviewer_signoff" + }, + { + "severity": "high", + "code": "TEMPLATE_MANIFEST_MISSING", + "message": "unapproved-preprint-style has no template checksum manifest.", + "refs": [ + "unapproved-preprint-style" + ], + "action": "attach_template_checksum_manifest" + }, + { + "severity": "medium", + "code": "OUTPUT_PROVENANCE_DIGEST_MISSING", + "message": "horizon-eu-repository-style has 1 generated outputs without build provenance digests.", + "refs": [ + "horizon-eu-repository-style" + ], + "action": "record_output_build_provenance_digests" + }, + { + "severity": "medium", + "code": "STYLE_VALIDATION_STALE", + "message": "horizon-eu-repository-style style validation is 173 days old.", + "refs": [ + "horizon-eu-repository-style" + ], + "action": "rerun_style_export_validation" + }, + { + "severity": "medium", + "code": "STYLE_VALIDATION_STALE", + "message": "unapproved-preprint-style style validation is missing.", + "refs": [ + "unapproved-preprint-style" + ], + "action": "rerun_style_export_validation" + }, + { + "severity": "medium", + "code": "TEMPLATE_CHECKSUM_INCOMPLETE", + "message": "templates/horizon/article.jats.xml lacks both expected and actual checksums.", + "refs": [ + "horizon-eu-repository-style" + ], + "action": "record_expected_and_actual_template_checksums" + } + ], + "packageSummaries": [ + { + "id": "horizon-eu-repository-style", + "pluginId": "journal-style-pack", + "pluginVersion": "3.5.0", + "target": "Horizon EU grant portal", + "formatCount": 1, + "templateCount": 2, + "validationAgeDays": 173, + "outputCount": 1 + }, + { + "id": "unapproved-preprint-style", + "pluginId": "community-style-snapshot", + "pluginVersion": "latest", + "target": "bioRxiv", + "formatCount": 2, + "templateCount": 0, + "validationAgeDays": null, + "outputCount": 0 + } + ], + "requiredExportFormats": [ + "JATS", + "DOCX", + "LaTeX" + ] + } +} diff --git a/enterprise-style-package-provenance-guard/reports/style-package-provenance-report.md b/enterprise-style-package-provenance-guard/reports/style-package-provenance-report.md new file mode 100644 index 00000000..6868aad1 --- /dev/null +++ b/enterprise-style-package-provenance-guard/reports/style-package-provenance-report.md @@ -0,0 +1,39 @@ +# Enterprise Style Package Provenance Report + +Institution: northstar-research-office +Review date: 2026-06-01 +Decision: hold_enterprise_style_exports +Audit digest: sha256:e519c9e6fe7731dd + +## Style Packages + +| Package | Target | Plugin | Formats | Templates | Validation age | +| --- | --- | --- | ---: | ---: | ---: | +| horizon-eu-repository-style | Horizon EU grant portal | journal-style-pack@3.5.0 | 1 | 2 | 173d | +| unapproved-preprint-style | bioRxiv | community-style-snapshot@latest | 2 | 0 | missing | + +## Findings + +- **CRITICAL PLUGIN_NOT_APPROVED**: unapproved-preprint-style uses plugin community-style-snapshot without enterprise approval. Action: `route_plugin_for_enterprise_approval`. +- **CRITICAL PRIVATE_FIELD_EXPORTED_IN_STYLE_PREVIEW**: horizon-eu-repository-style exports private internal style-preview fields. Action: `remove_private_preview_fields_from_export`. +- **CRITICAL TEMPLATE_CHECKSUM_DRIFT**: templates/horizon/report.docx checksum differs from the approved style package manifest. Action: `block_export_until_template_is_rebuilt_from_approved_source`. +- **HIGH CITATION_STYLE_MISMATCH**: horizon-eu-repository-style uses vancouver citations for Horizon EU grant portal which requires apa. Action: `select_target_approved_citation_style`. +- **HIGH EXPORT_FORMAT_COVERAGE_GAP**: horizon-eu-repository-style is missing required export formats: JATS, LaTeX. Action: `add_required_export_format_templates`. +- **HIGH EXPORT_FORMAT_COVERAGE_GAP**: unapproved-preprint-style is missing required export formats: LaTeX. Action: `add_required_export_format_templates`. +- **HIGH EXPORT_METADATA_PRESERVATION_GAP**: horizon-eu-repository-style does not preserve ORCID, version history, funder award metadata across export formats. Action: `preserve_required_publication_metadata`. +- **HIGH EXPORT_METADATA_PRESERVATION_GAP**: unapproved-preprint-style does not preserve DOI, ORCID, version history, license, funder award metadata across export formats. Action: `preserve_required_publication_metadata`. +- **HIGH PLUGIN_VERSION_NOT_APPROVED**: horizon-eu-repository-style uses 3.5.0, which is not in the approved plugin version list. Action: `promote_or_replace_approved_plugin_version`. +- **HIGH PLUGIN_VERSION_NOT_PINNED**: unapproved-preprint-style is missing a pinned semantic plugin version. Action: `pin_plugin_version_before_export`. +- **HIGH STYLE_SIGNOFF_MISSING**: horizon-eu-repository-style lacks approved enterprise reviewer signoff. Action: `obtain_enterprise_style_reviewer_signoff`. +- **HIGH STYLE_SIGNOFF_MISSING**: unapproved-preprint-style lacks approved enterprise reviewer signoff. Action: `obtain_enterprise_style_reviewer_signoff`. +- **HIGH TEMPLATE_MANIFEST_MISSING**: unapproved-preprint-style has no template checksum manifest. Action: `attach_template_checksum_manifest`. +- **MEDIUM OUTPUT_PROVENANCE_DIGEST_MISSING**: horizon-eu-repository-style has 1 generated outputs without build provenance digests. Action: `record_output_build_provenance_digests`. +- **MEDIUM STYLE_VALIDATION_STALE**: horizon-eu-repository-style style validation is 173 days old. Action: `rerun_style_export_validation`. +- **MEDIUM STYLE_VALIDATION_STALE**: unapproved-preprint-style style validation is missing. Action: `rerun_style_export_validation`. +- **MEDIUM TEMPLATE_CHECKSUM_INCOMPLETE**: templates/horizon/article.jats.xml lacks both expected and actual checksums. Action: `record_expected_and_actual_template_checksums`. + +## Required Export Formats + +- JATS +- DOCX +- LaTeX diff --git a/enterprise-style-package-provenance-guard/reports/summary.svg b/enterprise-style-package-provenance-guard/reports/summary.svg new file mode 100644 index 00000000..eab0c277 --- /dev/null +++ b/enterprise-style-package-provenance-guard/reports/summary.svg @@ -0,0 +1,8 @@ + + + +Enterprise Style Package Provenance +Decision: hold_enterprise_style_exports +critical3high10medium4 +Audit sha256:e519c9e6fe7731dd + diff --git a/enterprise-style-package-provenance-guard/sample-data.js b/enterprise-style-package-provenance-guard/sample-data.js new file mode 100644 index 00000000..8cf3a188 --- /dev/null +++ b/enterprise-style-package-provenance-guard/sample-data.js @@ -0,0 +1,161 @@ +const cleanPacket = { + institutionId: "northstar-research-office", + reviewDate: "2026-06-01", + maxValidationAgeDays: 90, + requiredExportFormats: ["JATS", "DOCX", "LaTeX"], + approvedPlugins: [ + { + id: "journal-style-pack", + allowedVersions: ["3.4.2", "3.4.3"], + owner: "enterprise-publication-ops" + } + ], + stylePackages: [ + { + id: "cell-reports-2026-style", + pluginId: "journal-style-pack", + pluginVersion: "3.4.3", + target: { + name: "Cell Reports", + citationStyle: "vancouver" + }, + formats: ["JATS", "DOCX", "LaTeX"], + citationStyle: "vancouver", + lastValidatedAt: "2026-05-20", + reviewerSignoff: { + reviewer: "pubops-style-reviewer", + status: "approved" + }, + templates: [ + { + path: "templates/cell-reports/article.jats.xml", + expectedChecksum: "sha256:6bd2f4c0c2a6", + actualChecksum: "sha256:6bd2f4c0c2a6" + }, + { + path: "templates/cell-reports/manuscript.docx", + expectedChecksum: "sha256:a9f28f1de342", + actualChecksum: "sha256:a9f28f1de342" + } + ], + metadataPreservation: { + doi: true, + orcid: true, + versionHistory: true, + license: true, + funderAward: true + }, + generatedOutputs: [ + { + path: "exports/cell-reports/article.xml", + builtFromTemplateChecksum: "sha256:6bd2f4c0c2a6", + provenanceDigest: "sha256:output-9122" + }, + { + path: "exports/cell-reports/manuscript.docx", + builtFromTemplateChecksum: "sha256:a9f28f1de342", + provenanceDigest: "sha256:output-8227" + } + ], + previewFields: [ + { + name: "publicTitle", + exported: true, + private: false + } + ] + } + ] +}; + +const riskyPacket = { + institutionId: "northstar-research-office", + reviewDate: "2026-06-01", + maxValidationAgeDays: 90, + requiredExportFormats: ["JATS", "DOCX", "LaTeX"], + approvedPlugins: [ + { + id: "journal-style-pack", + allowedVersions: ["3.4.2"], + owner: "enterprise-publication-ops" + } + ], + stylePackages: [ + { + id: "horizon-eu-repository-style", + pluginId: "journal-style-pack", + pluginVersion: "3.5.0", + target: { + name: "Horizon EU grant portal", + citationStyle: "apa" + }, + formats: ["DOCX"], + citationStyle: "vancouver", + lastValidatedAt: "2025-12-10", + reviewerSignoff: { + reviewer: "", + status: "draft" + }, + templates: [ + { + path: "templates/horizon/report.docx", + expectedChecksum: "sha256:approved-4411", + actualChecksum: "sha256:local-drift-7719" + }, + { + path: "templates/horizon/article.jats.xml", + expectedChecksum: "", + actualChecksum: "sha256:missing-expected" + } + ], + metadataPreservation: { + doi: true, + orcid: false, + versionHistory: false, + license: true, + funderAward: false + }, + generatedOutputs: [ + { + path: "exports/horizon/report.docx", + builtFromTemplateChecksum: "", + provenanceDigest: "" + } + ], + previewFields: [ + { + name: "internalReviewerNotes", + exported: true, + private: true + } + ] + }, + { + id: "unapproved-preprint-style", + pluginId: "community-style-snapshot", + pluginVersion: "latest", + target: { + name: "bioRxiv", + citationStyle: "vancouver" + }, + formats: ["JATS", "DOCX"], + citationStyle: "vancouver", + lastValidatedAt: null, + reviewerSignoff: null, + templates: [], + metadataPreservation: { + doi: false, + orcid: false, + versionHistory: false, + license: false, + funderAward: false + }, + generatedOutputs: [] + } + ] +}; + +module.exports = { + cleanPacket, + riskyPacket +}; diff --git a/enterprise-style-package-provenance-guard/test.js b/enterprise-style-package-provenance-guard/test.js new file mode 100644 index 00000000..2ede4c32 --- /dev/null +++ b/enterprise-style-package-provenance-guard/test.js @@ -0,0 +1,31 @@ +const assert = require("node:assert/strict"); +const { evaluateStylePackage, sha256 } = require("./index"); +const { cleanPacket, riskyPacket } = require("./sample-data"); + +const clean = evaluateStylePackage(cleanPacket); +assert.equal(clean.summary.decision, "release_enterprise_style_exports"); +assert.equal(clean.summary.findingCount, 0); +assert.equal(clean.summary.stylePackagesReviewed, 1); +assert.ok(clean.summary.auditDigest.startsWith("sha256:")); + +const risky = evaluateStylePackage(riskyPacket); +assert.equal(risky.summary.decision, "hold_enterprise_style_exports"); +assert.equal(risky.summary.stylePackagesReviewed, 2); +assert.ok(risky.summary.findingCount >= 10); +assert.ok(risky.summary.highOrCriticalFindings >= 7); + +const findingCodes = new Set(risky.findings.map((finding) => finding.code)); +assert.ok(findingCodes.has("TEMPLATE_CHECKSUM_DRIFT")); +assert.ok(findingCodes.has("PRIVATE_FIELD_EXPORTED_IN_STYLE_PREVIEW")); +assert.ok(findingCodes.has("PLUGIN_NOT_APPROVED")); +assert.ok(findingCodes.has("PLUGIN_VERSION_NOT_PINNED")); +assert.ok(findingCodes.has("EXPORT_METADATA_PRESERVATION_GAP")); +assert.ok(findingCodes.has("CITATION_STYLE_MISMATCH")); +assert.ok(findingCodes.has("EXPORT_FORMAT_COVERAGE_GAP")); + +const firstDigest = evaluateStylePackage(riskyPacket).summary.auditDigest; +const secondDigest = evaluateStylePackage(riskyPacket).summary.auditDigest; +assert.equal(firstDigest, secondDigest); +assert.equal(sha256({ b: 2, a: 1 }), sha256({ a: 1, b: 2 })); + +console.log("enterprise style package provenance guard tests passed");