diff --git a/collaborative-data-availability-statement-guard/README.md b/collaborative-data-availability-statement-guard/README.md new file mode 100644 index 00000000..f7990e73 --- /dev/null +++ b/collaborative-data-availability-statement-guard/README.md @@ -0,0 +1,35 @@ +# Collaborative Data Availability Statement Guard + +Self-contained Real-time Collaborative Research Editor slice for +`SCIBASE-AI/SCIBASE.AI#12`. + +The guard evaluates whether a collaborative manuscript can be exported with +complete data and code availability statements. It checks required availability +sections, repository accessions, statement citations, dataset/code licenses, +reviewer-only access windows, de-identification evidence for human-derived +material, role-based approvals, blocking comments, and unmerged editor changes. + +This is intentionally separate from reference-library merging, notification +visibility, accessibility, presence privacy, evidence binding, and general +embargo-release workflows. Its job is to gate the final manuscript export when +the availability statement and linked artifact evidence are not review-ready. + +## Run + +```bash +npm run check +npm test +npm run demo +npm run demo:video +``` + +## Outputs + +- `reports/summary.json` +- `reports/reviewer-packet.md` +- `reports/summary.svg` +- `reports/demo.webm` + +All data is synthetic. The module does not call repository hosts, journal +systems, identity services, storage APIs, email systems, or live manuscript +export services. diff --git a/collaborative-data-availability-statement-guard/demo-video.js b/collaborative-data-availability-statement-guard/demo-video.js new file mode 100644 index 00000000..3298eb2c --- /dev/null +++ b/collaborative-data-availability-statement-guard/demo-video.js @@ -0,0 +1,173 @@ +const fs = require("fs"); +const os = require("os"); +const path = require("path"); +const { execFileSync } = require("child_process"); + +const reportDir = path.join(__dirname, "reports"); +const outputPath = path.join(reportDir, "demo.webm"); + +const chromeCandidates = [ + process.env.CHROME_PATH, + "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe", + "C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe", + "C:\\Program Files\\Microsoft\\Edge\\Application\\msedge.exe", + "C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe" +].filter(Boolean); + +function findBrowser() { + const found = chromeCandidates.find((candidate) => fs.existsSync(candidate)); + if (!found) { + throw new Error("Chrome or Edge was not found. Set CHROME_PATH to generate reports/demo.webm."); + } + return found; +} + +function fileUrl(filePath) { + return `file:///${filePath.replace(/\\/g, "/")}`; +} + +const html = String.raw` + +
+ +recording+ + +`; + +fs.mkdirSync(reportDir, { recursive: true }); + +const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "data-availability-demo-")); +const htmlPath = path.join(tempDir, "demo.html"); +const profileDir = path.join(tempDir, "profile"); +fs.writeFileSync(htmlPath, html, "utf8"); + +const stdout = execFileSync( + findBrowser(), + [ + "--headless=new", + "--disable-gpu", + "--disable-dev-shm-usage", + "--autoplay-policy=no-user-gesture-required", + "--run-all-compositor-stages-before-draw", + "--virtual-time-budget=7500", + `--user-data-dir=${profileDir}`, + "--dump-dom", + fileUrl(htmlPath) + ], + { encoding: "utf8", maxBuffer: 30 * 1024 * 1024 } +); + +const match = stdout.match(/data:video\/webm;base64,([A-Za-z0-9+/=]+)/); +if (!match) { + throw new Error(`Demo video generation failed. Browser output ended with: ${stdout.slice(-600)}`); +} + +fs.writeFileSync(outputPath, Buffer.from(match[1], "base64")); +console.log(`Generated ${path.relative(process.cwd(), outputPath)}`); diff --git a/collaborative-data-availability-statement-guard/demo.js b/collaborative-data-availability-statement-guard/demo.js new file mode 100644 index 00000000..97c3730c --- /dev/null +++ b/collaborative-data-availability-statement-guard/demo.js @@ -0,0 +1,18 @@ +const fs = require("fs"); +const path = require("path"); +const { project } = require("./sample-data"); +const { buildReviewPacket, renderMarkdownReport, renderSvgSummary } = require("./index"); + +const reportDir = path.join(__dirname, "reports"); +fs.mkdirSync(reportDir, { recursive: true }); + +const packet = buildReviewPacket(project); + +fs.writeFileSync(path.join(reportDir, "summary.json"), `${JSON.stringify(packet, null, 2)}\n`, "utf8"); +fs.writeFileSync(path.join(reportDir, "reviewer-packet.md"), renderMarkdownReport(packet), "utf8"); +fs.writeFileSync(path.join(reportDir, "summary.svg"), renderSvgSummary(packet), "utf8"); + +console.log(`Generated reports for ${packet.guard}`); +console.log(`Decision: ${packet.decision}`); +console.log(`Score: ${packet.score}`); +console.log(`Findings: ${packet.findings.length}`); diff --git a/collaborative-data-availability-statement-guard/index.js b/collaborative-data-availability-statement-guard/index.js new file mode 100644 index 00000000..be1a95c9 --- /dev/null +++ b/collaborative-data-availability-statement-guard/index.js @@ -0,0 +1,320 @@ +const SEVERITY_WEIGHTS = { + critical: 36, + high: 22, + medium: 11, + low: 4 +}; + +function daysBetween(a, b) { + const left = new Date(a).getTime(); + const right = new Date(b).getTime(); + return Math.floor((right - left) / (24 * 60 * 60 * 1000)); +} + +function normalize(value) { + return String(value || "").trim().toLowerCase(); +} + +function addFinding(findings, severity, rule, message, action, refs = []) { + findings.push({ severity, rule, message, action, refs }); +} + +function sectionById(manuscript) { + return new Map(manuscript.sections.map((section) => [section.id, section])); +} + +function accessionPattern(kind) { + if (kind === "code") { + return /^(GH|GL|DOI)-[A-Za-z0-9._/-]+$/; + } + return /^(ZEN|DRYAD|FIGSHARE|OSF|DOI)-[A-Za-z0-9._/-]+$/; +} + +function referencedAccessions(manuscript) { + return new Set(manuscript.citations.map((citation) => normalize(citation.accessionId)).filter(Boolean)); +} + +function evaluateStatementReadiness(project) { + const findings = []; + const sections = sectionById(project.manuscript); + const citationAccessions = referencedAccessions(project.manuscript); + + for (const sectionId of project.policy.requiredSections) { + const section = sections.get(sectionId); + if (!section || normalize(section.text).length < 40) { + addFinding( + findings, + "critical", + "missing-required-availability-section", + `Required section ${sectionId} is missing or too thin for export.`, + "Block manuscript export until the collaborative editor contains a complete availability statement.", + [sectionId] + ); + } + } + + for (const citation of project.manuscript.citations) { + if (!sections.has(citation.sectionId)) { + addFinding( + findings, + "high", + "citation-anchor-missing", + `Citation ${citation.id} points at missing section ${citation.sectionId}.`, + "Repair the citation anchor before generating the final manuscript package.", + [citation.id, citation.sectionId] + ); + } + } + + for (const repository of project.repositories) { + const accession = normalize(repository.accessionId); + if (!accession) { + addFinding( + findings, + "critical", + "repository-accession-missing", + `${repository.label} has no stable accession or repository identifier.`, + "Attach a stable repository accession or mark the material as non-distributable with reviewer evidence.", + [repository.id] + ); + } else if (!accessionPattern(repository.kind).test(repository.accessionId)) { + addFinding( + findings, + "high", + "repository-accession-format-invalid", + `${repository.label} has accession ${repository.accessionId}, which does not match accepted export formats.`, + "Normalize the repository identifier before export so readers can resolve the artifact.", + [repository.id, repository.accessionId] + ); + } else if (!citationAccessions.has(accession)) { + addFinding( + findings, + "medium", + "repository-not-mentioned-in-statement", + `${repository.label} is registered but not cited in the availability sections.`, + "Add the repository accession to the data or code availability statement.", + [repository.id, repository.accessionId] + ); + } + + if (repository.kind === "dataset" && !project.policy.acceptedDatasetLicenses.includes(repository.license)) { + addFinding( + findings, + "high", + "dataset-license-missing-or-unaccepted", + `${repository.label} has dataset license ${repository.license || "none"}.`, + "Add an accepted dataset license or document why restricted access is required.", + [repository.id] + ); + } + + if (repository.kind === "code" && !project.policy.acceptedCodeLicenses.includes(repository.license)) { + addFinding( + findings, + "high", + "code-license-missing-or-unaccepted", + `${repository.label} has code license ${repository.license || "none"}.`, + "Add an accepted code license before exposing reproducibility controls.", + [repository.id] + ); + } + + if (repository.containsHumanDerivedData && !repository.deidentificationEvidenceId) { + addFinding( + findings, + "critical", + "human-derived-data-without-deidentification-evidence", + `${repository.label} contains human-derived material without de-identification evidence.`, + "Block export until the data steward links de-identification or restriction evidence.", + [repository.id] + ); + } + + if (repository.access === "embargoed" && repository.releaseDate) { + const releaseLag = daysBetween(project.manuscript.exportDeadline, repository.releaseDate); + if (releaseLag > project.policy.publicReleaseGraceDays) { + addFinding( + findings, + "medium", + "embargo-release-lags-export", + `${repository.label} releases ${releaseLag} days after the manuscript export deadline.`, + "Confirm the target journal accepts this availability timing before final export.", + [repository.id, repository.releaseDate] + ); + } + } + + if (repository.access !== "public" && repository.reviewerLinkExpiresAt) { + const reviewerDays = daysBetween(project.asOfDate, repository.reviewerLinkExpiresAt); + if (reviewerDays < project.policy.reviewerLinkMinimumDays) { + addFinding( + findings, + "high", + "reviewer-link-expires-before-review-window", + `${repository.label} reviewer access expires in ${reviewerDays} days.`, + "Refresh reviewer-only links before export so peer reviewers can inspect restricted artifacts.", + [repository.id, repository.reviewerLinkExpiresAt] + ); + } + } + } + + for (const role of project.policy.requiredApproverRoles) { + const approver = project.collaborators.find((collaborator) => collaborator.role === role); + if (!approver || approver.approval !== "approved") { + addFinding( + findings, + "high", + "required-availability-approver-missing", + `Required ${role} approval is not complete.`, + "Hold export until all role-based collaborators approve the availability statement.", + [role] + ); + } + } + + for (const comment of project.editorState.unresolvedComments) { + if (comment.severity === "blocking") { + addFinding( + findings, + "high", + "blocking-availability-comment-open", + `Blocking comment ${comment.id} remains open on ${comment.sectionId}.`, + "Resolve blocking availability comments before final manuscript export.", + [comment.id, comment.sectionId] + ); + } + } + + for (const change of project.editorState.pendingChanges) { + if (change.status !== "merged") { + addFinding( + findings, + "medium", + "availability-change-unmerged", + `Pending change ${change.id} in ${change.sectionId} has not been merged.`, + "Merge, reject, or explicitly defer the collaborative availability edit before export.", + [change.id, change.sectionId] + ); + } + } + + const severitySummary = findings.reduce( + (summary, finding) => { + summary[finding.severity] += 1; + return summary; + }, + { critical: 0, high: 0, medium: 0, low: 0 } + ); + const score = Math.max(0, 100 - findings.reduce((sum, finding) => sum + SEVERITY_WEIGHTS[finding.severity], 0)); + + return { findings, severitySummary, score }; +} + +function decisionFromEvaluation(evaluation) { + if (evaluation.severitySummary.critical > 0) { + return "block-export-until-availability-evidence-is-clean"; + } + if (evaluation.score < 75 || evaluation.severitySummary.high > 0) { + return "hold-export-for-availability-review"; + } + if (evaluation.score < 90) { + return "manual-review-before-export"; + } + return "availability-statement-ready"; +} + +function buildReviewerActions(findings) { + return findings.map((finding) => ({ + priority: finding.severity === "critical" || finding.severity === "high" ? "blocking" : "review", + rule: finding.rule, + action: finding.action, + refs: finding.refs + })); +} + +function buildReviewPacket(project) { + const evaluation = evaluateStatementReadiness(project); + return { + guard: "collaborative-data-availability-statement-guard", + issue: "SCIBASE-AI/SCIBASE.AI#12", + manuscriptId: project.manuscript.id, + title: project.manuscript.title, + targetJournal: project.manuscript.targetJournal, + asOfDate: project.asOfDate, + decision: decisionFromEvaluation(evaluation), + score: evaluation.score, + severitySummary: evaluation.severitySummary, + findings: evaluation.findings, + reviewerActions: buildReviewerActions(evaluation.findings), + safety: [ + "Synthetic manuscript, repository, collaborator, and review data only", + "No GitHub, Zenodo, journal, identity, storage, or email network calls", + "No private manuscript content, human-subject records, credentials, or live export mutations" + ] + }; +} + +function renderMarkdownReport(packet) { + const lines = [ + "# Collaborative Data Availability Statement Guard", + "", + `Manuscript: ${packet.title}`, + `Issue: ${packet.issue}`, + `Decision: ${packet.decision}`, + `Score: ${packet.score}`, + "", + "## Severity Summary", + "", + "| Severity | Count |", + "| --- | ---: |" + ]; + + for (const severity of ["critical", "high", "medium", "low"]) { + lines.push(`| ${severity} | ${packet.severitySummary[severity]} |`); + } + + lines.push("", "## Findings", ""); + for (const finding of packet.findings) { + lines.push(`- **${finding.severity} / ${finding.rule}**: ${finding.message}`); + lines.push(` - Action: ${finding.action}`); + lines.push(` - Refs: ${finding.refs.join(", ") || "none"}`); + } + + lines.push("", "## Safety", ""); + for (const item of packet.safety) { + lines.push(`- ${item}`); + } + + return `${lines.join("\n")}\n`; +} + +function renderSvgSummary(packet) { + const barWidth = Math.max(44, Math.min(760, packet.score * 7.6)); + const totalFindings = packet.findings.length; + return ` +`; +} + +module.exports = { + buildReviewPacket, + decisionFromEvaluation, + evaluateStatementReadiness, + renderMarkdownReport, + renderSvgSummary +}; diff --git a/collaborative-data-availability-statement-guard/package.json b/collaborative-data-availability-statement-guard/package.json new file mode 100644 index 00000000..bc525426 --- /dev/null +++ b/collaborative-data-availability-statement-guard/package.json @@ -0,0 +1,14 @@ +{ + "name": "collaborative-data-availability-statement-guard", + "version": "1.0.0", + "description": "Deterministic export guard for collaborative manuscript data and code availability statements.", + "main": "index.js", + "private": true, + "type": "commonjs", + "scripts": { + "check": "node --check index.js && node --check sample-data.js && node --check demo.js && node --check demo-video.js && node --check test.js", + "test": "node test.js", + "demo": "node demo.js", + "demo:video": "node demo-video.js" + } +} diff --git a/collaborative-data-availability-statement-guard/reports/demo.webm b/collaborative-data-availability-statement-guard/reports/demo.webm new file mode 100644 index 00000000..7b86aeca Binary files /dev/null and b/collaborative-data-availability-statement-guard/reports/demo.webm differ diff --git a/collaborative-data-availability-statement-guard/reports/reviewer-packet.md b/collaborative-data-availability-statement-guard/reports/reviewer-packet.md new file mode 100644 index 00000000..b7c5bd68 --- /dev/null +++ b/collaborative-data-availability-statement-guard/reports/reviewer-packet.md @@ -0,0 +1,51 @@ +# Collaborative Data Availability Statement Guard + +Manuscript: Joint Galaxy Morphology Review +Issue: SCIBASE-AI/SCIBASE.AI#12 +Decision: block-export-until-availability-evidence-is-clean +Score: 0 + +## Severity Summary + +| Severity | Count | +| --- | ---: | +| critical | 2 | +| high | 5 | +| medium | 2 | +| low | 0 | + +## Findings + +- **medium / embargo-release-lags-export**: Processed synthetic tables releases 11 days after the manuscript export deadline. + - Action: Confirm the target journal accepts this availability timing before final export. + - Refs: repo-data-processed, 2026-06-05 +- **high / reviewer-link-expires-before-review-window**: Processed synthetic tables reviewer access expires in 1 days. + - Action: Refresh reviewer-only links before export so peer reviewers can inspect restricted artifacts. + - Refs: repo-data-processed, 2026-05-23 +- **critical / repository-accession-missing**: Reviewer note extracts has no stable accession or repository identifier. + - Action: Attach a stable repository accession or mark the material as non-distributable with reviewer evidence. + - Refs: repo-data-raw +- **high / dataset-license-missing-or-unaccepted**: Reviewer note extracts has dataset license none. + - Action: Add an accepted dataset license or document why restricted access is required. + - Refs: repo-data-raw +- **critical / human-derived-data-without-deidentification-evidence**: Reviewer note extracts contains human-derived material without de-identification evidence. + - Action: Block export until the data steward links de-identification or restriction evidence. + - Refs: repo-data-raw +- **high / reviewer-link-expires-before-review-window**: Reviewer note extracts reviewer access expires in -3 days. + - Action: Refresh reviewer-only links before export so peer reviewers can inspect restricted artifacts. + - Refs: repo-data-raw, 2026-05-19 +- **high / required-availability-approver-missing**: Required data-steward approval is not complete. + - Action: Hold export until all role-based collaborators approve the availability statement. + - Refs: data-steward +- **high / blocking-availability-comment-open**: Blocking comment comment-17 remains open on data-availability. + - Action: Resolve blocking availability comments before final manuscript export. + - Refs: comment-17, data-availability +- **medium / availability-change-unmerged**: Pending change change-9 in data-availability has not been merged. + - Action: Merge, reject, or explicitly defer the collaborative availability edit before export. + - Refs: change-9, data-availability + +## Safety + +- Synthetic manuscript, repository, collaborator, and review data only +- No GitHub, Zenodo, journal, identity, storage, or email network calls +- No private manuscript content, human-subject records, credentials, or live export mutations diff --git a/collaborative-data-availability-statement-guard/reports/summary.json b/collaborative-data-availability-statement-guard/reports/summary.json new file mode 100644 index 00000000..50ca7593 --- /dev/null +++ b/collaborative-data-availability-statement-guard/reports/summary.json @@ -0,0 +1,188 @@ +{ + "guard": "collaborative-data-availability-statement-guard", + "issue": "SCIBASE-AI/SCIBASE.AI#12", + "manuscriptId": "ms-collab-astro-042", + "title": "Joint Galaxy Morphology Review", + "targetJournal": "Synthetic Journal of Open Science", + "asOfDate": "2026-05-22", + "decision": "block-export-until-availability-evidence-is-clean", + "score": 0, + "severitySummary": { + "critical": 2, + "high": 5, + "medium": 2, + "low": 0 + }, + "findings": [ + { + "severity": "medium", + "rule": "embargo-release-lags-export", + "message": "Processed synthetic tables releases 11 days after the manuscript export deadline.", + "action": "Confirm the target journal accepts this availability timing before final export.", + "refs": [ + "repo-data-processed", + "2026-06-05" + ] + }, + { + "severity": "high", + "rule": "reviewer-link-expires-before-review-window", + "message": "Processed synthetic tables reviewer access expires in 1 days.", + "action": "Refresh reviewer-only links before export so peer reviewers can inspect restricted artifacts.", + "refs": [ + "repo-data-processed", + "2026-05-23" + ] + }, + { + "severity": "critical", + "rule": "repository-accession-missing", + "message": "Reviewer note extracts has no stable accession or repository identifier.", + "action": "Attach a stable repository accession or mark the material as non-distributable with reviewer evidence.", + "refs": [ + "repo-data-raw" + ] + }, + { + "severity": "high", + "rule": "dataset-license-missing-or-unaccepted", + "message": "Reviewer note extracts has dataset license none.", + "action": "Add an accepted dataset license or document why restricted access is required.", + "refs": [ + "repo-data-raw" + ] + }, + { + "severity": "critical", + "rule": "human-derived-data-without-deidentification-evidence", + "message": "Reviewer note extracts contains human-derived material without de-identification evidence.", + "action": "Block export until the data steward links de-identification or restriction evidence.", + "refs": [ + "repo-data-raw" + ] + }, + { + "severity": "high", + "rule": "reviewer-link-expires-before-review-window", + "message": "Reviewer note extracts reviewer access expires in -3 days.", + "action": "Refresh reviewer-only links before export so peer reviewers can inspect restricted artifacts.", + "refs": [ + "repo-data-raw", + "2026-05-19" + ] + }, + { + "severity": "high", + "rule": "required-availability-approver-missing", + "message": "Required data-steward approval is not complete.", + "action": "Hold export until all role-based collaborators approve the availability statement.", + "refs": [ + "data-steward" + ] + }, + { + "severity": "high", + "rule": "blocking-availability-comment-open", + "message": "Blocking comment comment-17 remains open on data-availability.", + "action": "Resolve blocking availability comments before final manuscript export.", + "refs": [ + "comment-17", + "data-availability" + ] + }, + { + "severity": "medium", + "rule": "availability-change-unmerged", + "message": "Pending change change-9 in data-availability has not been merged.", + "action": "Merge, reject, or explicitly defer the collaborative availability edit before export.", + "refs": [ + "change-9", + "data-availability" + ] + } + ], + "reviewerActions": [ + { + "priority": "review", + "rule": "embargo-release-lags-export", + "action": "Confirm the target journal accepts this availability timing before final export.", + "refs": [ + "repo-data-processed", + "2026-06-05" + ] + }, + { + "priority": "blocking", + "rule": "reviewer-link-expires-before-review-window", + "action": "Refresh reviewer-only links before export so peer reviewers can inspect restricted artifacts.", + "refs": [ + "repo-data-processed", + "2026-05-23" + ] + }, + { + "priority": "blocking", + "rule": "repository-accession-missing", + "action": "Attach a stable repository accession or mark the material as non-distributable with reviewer evidence.", + "refs": [ + "repo-data-raw" + ] + }, + { + "priority": "blocking", + "rule": "dataset-license-missing-or-unaccepted", + "action": "Add an accepted dataset license or document why restricted access is required.", + "refs": [ + "repo-data-raw" + ] + }, + { + "priority": "blocking", + "rule": "human-derived-data-without-deidentification-evidence", + "action": "Block export until the data steward links de-identification or restriction evidence.", + "refs": [ + "repo-data-raw" + ] + }, + { + "priority": "blocking", + "rule": "reviewer-link-expires-before-review-window", + "action": "Refresh reviewer-only links before export so peer reviewers can inspect restricted artifacts.", + "refs": [ + "repo-data-raw", + "2026-05-19" + ] + }, + { + "priority": "blocking", + "rule": "required-availability-approver-missing", + "action": "Hold export until all role-based collaborators approve the availability statement.", + "refs": [ + "data-steward" + ] + }, + { + "priority": "blocking", + "rule": "blocking-availability-comment-open", + "action": "Resolve blocking availability comments before final manuscript export.", + "refs": [ + "comment-17", + "data-availability" + ] + }, + { + "priority": "review", + "rule": "availability-change-unmerged", + "action": "Merge, reject, or explicitly defer the collaborative availability edit before export.", + "refs": [ + "change-9", + "data-availability" + ] + } + ], + "safety": [ + "Synthetic manuscript, repository, collaborator, and review data only", + "No GitHub, Zenodo, journal, identity, storage, or email network calls", + "No private manuscript content, human-subject records, credentials, or live export mutations" + ] +} diff --git a/collaborative-data-availability-statement-guard/reports/summary.svg b/collaborative-data-availability-statement-guard/reports/summary.svg new file mode 100644 index 00000000..623c4d01 --- /dev/null +++ b/collaborative-data-availability-statement-guard/reports/summary.svg @@ -0,0 +1,16 @@ + diff --git a/collaborative-data-availability-statement-guard/requirements-map.md b/collaborative-data-availability-statement-guard/requirements-map.md new file mode 100644 index 00000000..4b73c06a --- /dev/null +++ b/collaborative-data-availability-statement-guard/requirements-map.md @@ -0,0 +1,18 @@ +# Requirements Map + +Issue: `SCIBASE-AI/SCIBASE.AI#12` + +| Issue requirement | Implementation | +| --- | --- | +| Real-time collaborative research editor | Models collaborative manuscript sections, comments, pending changes, and role-based author approvals before export. | +| Manuscript export readiness | Blocks final export when availability statements, repository citations, reviewer access, or collaborator approvals are incomplete. | +| Research artifact handling | Validates dataset and code repository accessions, artifact licenses, checksums, and human-derived data safeguards. | +| Reviewer-safe workflow | Produces deterministic JSON, Markdown, SVG, and video artifacts for local reviewer inspection without external services. | +| Non-overlap with existing slices | Focuses on availability statement export gating rather than reference merges, notifications, accessibility, presence, evidence binding, or general embargo automation. | +| Safe local validation | Includes dependency-free tests and demo generation from synthetic manuscript and repository metadata only. | + +## Non-goals + +- No live GitHub, Zenodo, OSF, journal, identity, storage, or email calls. +- No private manuscripts, credentials, human-subject records, or reviewer data. +- No mutation of collaborative editor documents or repository permissions. diff --git a/collaborative-data-availability-statement-guard/sample-data.js b/collaborative-data-availability-statement-guard/sample-data.js new file mode 100644 index 00000000..c9df16a1 --- /dev/null +++ b/collaborative-data-availability-statement-guard/sample-data.js @@ -0,0 +1,106 @@ +const project = { + asOfDate: "2026-05-22", + manuscript: { + id: "ms-collab-astro-042", + title: "Joint Galaxy Morphology Review", + targetJournal: "Synthetic Journal of Open Science", + exportDeadline: "2026-05-25", + sections: [ + { + id: "methods", + title: "Methods", + text: + "The study used a synthetic morphology corpus with reviewer-only raw tables and a scripted notebook pipeline." + }, + { + id: "data-availability", + title: "Data Availability", + text: + "Processed tables are available at Zenodo accession ZEN-2026-7788 after acceptance. Raw human-derived review notes are restricted and released as de-identified summaries only." + }, + { + id: "code-availability", + title: "Code Availability", + text: + "Analysis notebooks are available at GitHub repository synthetic-lab/galaxy-review under the MIT license." + } + ], + citations: [ + { id: "cite-dataset", kind: "dataset", accessionId: "ZEN-2026-7788", sectionId: "data-availability" }, + { id: "cite-notebook", kind: "code", accessionId: "GH-synthetic-lab-galaxy-review", sectionId: "code-availability" } + ] + }, + collaborators: [ + { id: "alice", role: "corresponding-author", approval: "approved", approvedAt: "2026-05-20" }, + { id: "devon", role: "data-steward", approval: "pending", approvedAt: null }, + { id: "mira", role: "code-owner", approval: "approved", approvedAt: "2026-05-21" } + ], + editorState: { + lastExportAttemptAt: "2026-05-22T15:20:00Z", + unresolvedComments: [ + { + id: "comment-17", + sectionId: "data-availability", + severity: "blocking", + text: "Confirm reviewer-only link expiry before export." + } + ], + pendingChanges: [ + { id: "change-9", sectionId: "data-availability", authorId: "devon", status: "unmerged" } + ] + }, + repositories: [ + { + id: "repo-data-processed", + label: "Processed synthetic tables", + kind: "dataset", + accessionId: "ZEN-2026-7788", + provider: "Zenodo", + access: "embargoed", + releaseDate: "2026-06-05", + reviewerLinkExpiresAt: "2026-05-23", + license: "CC-BY-4.0", + containsHumanDerivedData: false, + deidentificationEvidenceId: null, + checksum: "sha256:3e1a9bff5781" + }, + { + id: "repo-data-raw", + label: "Reviewer note extracts", + kind: "dataset", + accessionId: "", + provider: "Institutional vault", + access: "restricted", + releaseDate: null, + reviewerLinkExpiresAt: "2026-05-19", + license: "", + containsHumanDerivedData: true, + deidentificationEvidenceId: "", + checksum: "sha256:rawnotes9" + }, + { + id: "repo-code-notebooks", + label: "Notebook pipeline", + kind: "code", + accessionId: "GH-synthetic-lab-galaxy-review", + provider: "GitHub", + access: "public", + releaseDate: "2026-05-20", + reviewerLinkExpiresAt: null, + license: "MIT", + containsHumanDerivedData: false, + deidentificationEvidenceId: null, + checksum: "sha256:notebook221" + } + ], + policy: { + requiredSections: ["data-availability", "code-availability"], + requiredApproverRoles: ["corresponding-author", "data-steward", "code-owner"], + acceptedDatasetLicenses: ["CC-BY-4.0", "CC0-1.0", "ODC-BY-1.0"], + acceptedCodeLicenses: ["MIT", "Apache-2.0", "BSD-3-Clause"], + reviewerLinkMinimumDays: 5, + publicReleaseGraceDays: 7 + } +}; + +module.exports = { project }; diff --git a/collaborative-data-availability-statement-guard/test.js b/collaborative-data-availability-statement-guard/test.js new file mode 100644 index 00000000..23874cad --- /dev/null +++ b/collaborative-data-availability-statement-guard/test.js @@ -0,0 +1,70 @@ +const assert = require("assert"); +const { project } = require("./sample-data"); +const { + buildReviewPacket, + evaluateStatementReadiness, + renderMarkdownReport, + renderSvgSummary +} = require("./index"); + +const evaluation = evaluateStatementReadiness(project); +const packet = buildReviewPacket(project); + +assert.strictEqual(packet.guard, "collaborative-data-availability-statement-guard"); +assert.strictEqual(packet.issue, "SCIBASE-AI/SCIBASE.AI#12"); +assert.strictEqual(packet.decision, "block-export-until-availability-evidence-is-clean"); +assert.ok(evaluation.score < 70, "expected low readiness score for blocked export"); + +assert.ok( + evaluation.findings.some((finding) => finding.rule === "repository-accession-missing"), + "expected missing repository accession finding" +); +assert.ok( + evaluation.findings.some((finding) => finding.rule === "human-derived-data-without-deidentification-evidence"), + "expected human-derived data finding" +); +assert.ok( + evaluation.findings.some((finding) => finding.rule === "reviewer-link-expires-before-review-window"), + "expected reviewer link expiry finding" +); +assert.ok( + evaluation.findings.some((finding) => finding.rule === "required-availability-approver-missing"), + "expected missing approver finding" +); +assert.ok( + evaluation.findings.some((finding) => finding.rule === "blocking-availability-comment-open"), + "expected blocking comment finding" +); + +const cleanProject = JSON.parse(JSON.stringify(project)); +cleanProject.collaborators.find((collaborator) => collaborator.role === "data-steward").approval = "approved"; +cleanProject.collaborators.find((collaborator) => collaborator.role === "data-steward").approvedAt = "2026-05-22"; +cleanProject.editorState.unresolvedComments = []; +cleanProject.editorState.pendingChanges = []; +cleanProject.repositories.find((repository) => repository.id === "repo-data-processed").releaseDate = "2026-05-29"; +cleanProject.repositories.find((repository) => repository.id === "repo-data-processed").reviewerLinkExpiresAt = "2026-06-10"; +const rawRepo = cleanProject.repositories.find((repository) => repository.id === "repo-data-raw"); +rawRepo.accessionId = "OSF-raw-review-notes-restricted"; +rawRepo.license = "CC-BY-4.0"; +rawRepo.reviewerLinkExpiresAt = "2026-06-10"; +rawRepo.deidentificationEvidenceId = "deid-attestation-2026-05"; +cleanProject.manuscript.citations.push({ + id: "cite-raw-restricted", + kind: "dataset", + accessionId: "OSF-raw-review-notes-restricted", + sectionId: "data-availability" +}); + +const cleanPacket = buildReviewPacket(cleanProject); +assert.strictEqual(cleanPacket.decision, "availability-statement-ready"); +assert.strictEqual(cleanPacket.findings.length, 0); + +const markdown = renderMarkdownReport(packet); +assert.ok(markdown.includes("## Findings")); +assert.ok(markdown.includes("repository-accession-missing")); + +const svg = renderSvgSummary(packet); +assert.ok(svg.includes("