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
69 changes: 69 additions & 0 deletions institutional-repository-sync-sla-guard/demo.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
const fs = require("fs");
const path = require("path");

const { evaluateSyncJobs } = require("./index");
const { syncJobs } = require("./sample-data");

const reportDir = path.join(__dirname, "reports");
fs.mkdirSync(reportDir, { recursive: true });

const report = evaluateSyncJobs(syncJobs);
fs.writeFileSync(path.join(reportDir, "repository-sync-sla-report.json"), `${JSON.stringify(report, null, 2)}\n`);

const lines = [
"# Institutional Repository Sync SLA Guard",
"",
`Generated: ${report.generatedAt}`,
`Report digest: \`${report.reportDigest}\``,
"",
"## Summary",
"",
`- Total jobs: ${report.summary.total}`,
`- Ready: ${report.summary.ready}`,
`- Needs review: ${report.summary.needs_review}`,
`- Blocked: ${report.summary.blocked}`,
`- Findings: ${report.summary.findingCount}`,
"",
"## Decisions",
"",
];

for (const decision of report.decisions) {
lines.push(`### ${decision.id}`);
lines.push("");
lines.push(`- Decision: **${decision.decision}**`);
lines.push(`- Project: ${decision.project}`);
lines.push(`- Target: ${decision.targetRepository} (${decision.targetType})`);
lines.push(`- Escalation: \`${decision.escalation}\``);
lines.push(`- Audit digest: \`${decision.auditDigest}\``);
lines.push("- Findings:");
const findings = decision.findings.length
? decision.findings
: [{ severity: "info", code: "none", message: "No blocking or review findings." }];
for (const finding of findings) {
lines.push(` - ${finding.severity}: ${finding.code} - ${finding.message}`);
}
lines.push("");
}
if (lines[lines.length - 1] === "") {
lines.pop();
}
fs.writeFileSync(path.join(reportDir, "repository-sync-sla-report.md"), `${lines.join("\n")}\n`);

const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="960" height="540" viewBox="0 0 960 540">
<rect width="960" height="540" fill="#eef3f7"/>
<rect x="54" y="52" width="852" height="436" rx="8" fill="#ffffff" stroke="#1f3349" stroke-width="3"/>
<text x="86" y="112" font-family="Georgia, serif" font-size="34" fill="#1f3349">Institutional Repository Sync SLA Guard</text>
<text x="88" y="156" font-family="Menlo, monospace" font-size="18" fill="#42566f">Enterprise export-pipeline reviewer artifact</text>
<rect x="96" y="210" width="${Math.max(1, report.summary.ready) * 120}" height="54" fill="#2f855a"/>
<rect x="96" y="292" width="${Math.max(1, report.summary.needs_review) * 120}" height="54" fill="#b7791f"/>
<rect x="96" y="374" width="${Math.max(1, report.summary.blocked) * 120}" height="54" fill="#b83232"/>
<text x="112" y="245" font-family="Menlo, monospace" font-size="22" fill="#ffffff">ready ${report.summary.ready}</text>
<text x="112" y="327" font-family="Menlo, monospace" font-size="22" fill="#ffffff">review ${report.summary.needs_review}</text>
<text x="112" y="409" font-family="Menlo, monospace" font-size="22" fill="#ffffff">blocked ${report.summary.blocked}</text>
<text x="86" y="464" font-family="Menlo, monospace" font-size="16" fill="#42566f">Digest ${report.reportDigest.slice(0, 32)}...</text>
</svg>
`;
fs.writeFileSync(path.join(reportDir, "summary.svg"), svg);

console.log(JSON.stringify(report.summary, null, 2));
131 changes: 131 additions & 0 deletions institutional-repository-sync-sla-guard/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
const crypto = require("crypto");

function digest(value) {
return crypto.createHash("sha256").update(JSON.stringify(value)).digest("hex");
}

function embargoDays(embargoUntil, now = new Date("2026-05-29T00:00:00Z")) {
if (!embargoUntil) return 0;
const embargoDate = new Date(`${embargoUntil}T00:00:00Z`);
if (Number.isNaN(embargoDate.getTime())) return 0;
return Math.ceil((embargoDate.getTime() - now.getTime()) / 86_400_000);
}

function evaluateJob(job) {
const findings = [];

if (job.mandate.dueInHours < 0) {
findings.push({
severity: "blocker",
code: "mandate-deadline-missed",
message: `${job.mandate.funder} repository sync is ${Math.abs(job.mandate.dueInHours)} hour(s) overdue.`,
});
} else if (job.mandate.dueInHours <= 48) {
findings.push({
severity: "review",
code: "mandate-deadline-near",
message: `${job.mandate.funder} deadline is within ${job.mandate.dueInHours} hour(s).`,
});
}

if (!job.package.metadataComplete) {
findings.push({
severity: "blocker",
code: "metadata-incomplete",
message: "Repository export metadata is incomplete.",
});
}

if (job.mandate.requiresDoi && !job.package.doiPresent) {
findings.push({
severity: "blocker",
code: "missing-doi",
message: "Target repository or funder mandate requires DOI preservation.",
});
}

if (job.mandate.requiresOrcid && job.package.orcidCoveragePercent < 90) {
findings.push({
severity: "review",
code: "low-orcid-coverage",
message: `ORCID coverage is ${job.package.orcidCoveragePercent}%, below the 90% admin threshold.`,
});
}

if (!job.package.artifactHashPinned) {
findings.push({
severity: "blocker",
code: "unpinned-artifact-hash",
message: "Export package must pin artifact hashes before repository sync.",
});
}

const holdDays = embargoDays(job.package.embargoUntil);
if (holdDays > 0) {
findings.push({
severity: "blocker",
code: "active-embargo",
message: `Repository sync is blocked by an active embargo for ${holdDays} day(s).`,
});
}

if (job.operations.queuedHours >= 24 || job.operations.retryCount >= 3) {
findings.push({
severity: "review",
code: "sync-backlog-risk",
message: `Sync has queued for ${job.operations.queuedHours} hour(s) with ${job.operations.retryCount} retry attempt(s).`,
});
}

if (job.operations.lastWebhookStatus === "failed") {
findings.push({
severity: "review",
code: "webhook-delivery-failed",
message: "Last institutional webhook delivery failed and requires admin visibility.",
});
}

const blockers = findings.filter((finding) => finding.severity === "blocker");
const reviews = findings.filter((finding) => finding.severity === "review");
const decision = blockers.length ? "blocked" : reviews.length ? "needs_review" : "ready";

return {
id: job.id,
project: job.project,
targetRepository: job.target.repository,
targetType: job.target.type,
adminOwner: job.operations.adminOwner,
decision,
findings,
escalation: decision === "ready" ? "release-to-sync-worker" : `route-to-${job.operations.adminOwner}`,
auditDigest: digest({
id: job.id,
decision,
findings: findings.map((finding) => finding.code),
target: job.target,
mandate: job.mandate,
}),
};
}

function evaluateSyncJobs(jobs) {
const decisions = jobs.map(evaluateJob);
const summary = decisions.reduce(
(acc, decision) => {
acc.total += 1;
acc[decision.decision] += 1;
acc.findingCount += decision.findings.length;
return acc;
},
{ total: 0, ready: 0, needs_review: 0, blocked: 0, findingCount: 0 }
);
return {
generatedAt: "2026-05-29T00:00:00.000Z",
guard: "institutional-repository-sync-sla-guard",
summary,
decisions,
reportDigest: digest({ summary, decisions }),
};
}

module.exports = { evaluateJob, evaluateSyncJobs, embargoDays };
27 changes: 27 additions & 0 deletions institutional-repository-sync-sla-guard/readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Institutional Repository Sync SLA Guard

This is a focused Enterprise Tooling slice for SCIBASE export pipelines. It evaluates institutional repository sync jobs before they are released to external systems such as Zenodo, PubMed Central, arXiv, Invenio, DSpace, funder portals, or institutional archives.

The module uses synthetic reviewer scenarios only. It does not call external repositories, APIs, webhooks, identity providers, payment systems, live projects, private user data, credentials, or tokens.

## What It Checks

- Funder mandate deadlines and overdue sync jobs.
- Metadata completeness, DOI preservation, ORCID coverage, and artifact hash pins.
- Embargo holds before export.
- Backlog/retry risk and webhook delivery visibility for enterprise admins.
- Deterministic audit digests and escalation routes.

## Run

```bash
node institutional-repository-sync-sla-guard/test.js
node institutional-repository-sync-sla-guard/demo.js
node institutional-repository-sync-sla-guard/render-video.js
```

Generated reviewer artifacts are written to `institutional-repository-sync-sla-guard/reports/`.

## Scope Boundary

This slice is distinct from existing #19 work around enterprise dashboards, institutional admin auditing, generic API/webhook integrations, API rate limiting, webhook delivery, accessibility, repository/archive export, artifact hosting, and broad integration modules. It focuses specifically on SLA and compliance governance for institutional repository sync backlogs.
73 changes: 73 additions & 0 deletions institutional-repository-sync-sla-guard/render-video.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
const { execFileSync } = require("child_process");
const fs = require("fs");
const path = require("path");

const { evaluateSyncJobs } = require("./index");
const { syncJobs } = require("./sample-data");

const reportDir = path.join(__dirname, "reports");
const framePath = path.join(reportDir, "demo-frame.ppm");
const mp4Path = path.join(reportDir, "demo.mp4");
const report = evaluateSyncJobs(syncJobs);

function rgb(hex) {
return [
Number.parseInt(hex.slice(0, 2), 16),
Number.parseInt(hex.slice(2, 4), 16),
Number.parseInt(hex.slice(4, 6), 16),
];
}

function rect(buffer, width, x, y, w, h, color) {
const [r, g, b] = rgb(color);
for (let row = y; row < y + h; row += 1) {
for (let col = x; col < x + w; col += 1) {
const idx = (row * width + col) * 3;
buffer[idx] = r;
buffer[idx + 1] = g;
buffer[idx + 2] = b;
}
}
}

const width = 960;
const height = 540;
const pixels = Buffer.alloc(width * height * 3);
for (let i = 0; i < pixels.length; i += 3) {
pixels[i] = 0xee;
pixels[i + 1] = 0xf3;
pixels[i + 2] = 0xf7;
}
rect(pixels, width, 54, 52, 852, 436, "ffffff");
rect(pixels, width, 54, 52, 852, 4, "1f3349");
rect(pixels, width, 54, 484, 852, 4, "1f3349");
rect(pixels, width, 54, 52, 4, 436, "1f3349");
rect(pixels, width, 902, 52, 4, 436, "1f3349");
rect(pixels, width, 96, 180, Math.max(1, report.summary.ready) * 120, 58, "2f855a");
rect(pixels, width, 96, 270, Math.max(1, report.summary.needs_review) * 120, 58, "b7791f");
rect(pixels, width, 96, 360, Math.max(1, report.summary.blocked) * 120, 58, "b83232");
rect(pixels, width, 96, 450, 720, 12, "42566f");
fs.writeFileSync(framePath, Buffer.concat([Buffer.from(`P6\n${width} ${height}\n255\n`), pixels]));

execFileSync(
"ffmpeg",
[
"-y",
"-loop",
"1",
"-framerate",
"24",
"-i",
framePath,
"-t",
"5",
"-vf",
"format=yuv420p",
"-movflags",
"+faststart",
mp4Path,
],
{ stdio: "inherit" }
);

console.log(mp4Path);
Binary file not shown.
Loading