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
38 changes: 38 additions & 0 deletions geospatial-sample-provenance-guard/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Geospatial Sample Provenance Guard

Self-contained Scientific Knowledge Graph Integration slice for
`SCIBASE-AI/SCIBASE.AI#17`.

The guard evaluates field-sample and specimen location graph edges before they
appear on entity pages or public discovery recommendations. It checks coordinate
ranges, CRS normalization, country/coordinate consistency, coordinate precision,
sensitive-site redaction, voucher identifiers, dataset DOI resolution,
collection-date plausibility, and sample-to-dataset edge alignment.

This is intentionally separate from broad graph extraction/navigation, link
audit, ontology drift/alias/synonym controls, relationship conflict arbitration,
author-affiliation disambiguation, artifact lineage, evidence freshness,
instrument-method compatibility, reproducibility routes, recommendation
visibility/diversity, negative-result replication, measurement harmonization,
claim qualifier, ethics provenance, funder award lineage, clinical trial
registry, and software/runtime compatibility slices.

## 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 geocoders, repositories, GIS
systems, ontology services, specimen registries, journal systems, or external
APIs.
173 changes: 173 additions & 0 deletions geospatial-sample-provenance-guard/demo-video.js
Original file line number Diff line number Diff line change
@@ -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`<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Geospatial sample provenance guard demo</title>
<style>
html, body { margin: 0; background: #f8fafc; }
canvas { width: 960px; height: 540px; }
pre { white-space: pre-wrap; word-break: break-all; color: #f8fafc; font-size: 1px; }
</style>
</head>
<body>
<canvas id="stage" width="960" height="540"></canvas>
<pre id="out">recording</pre>
<script>
const canvas = document.getElementById("stage");
const ctx = canvas.getContext("2d");
const out = document.getElementById("out");
const checks = [
["Coordinate validity", "Latitude, longitude, country bounds, and CRS must align."],
["Sensitive sites", "Endangered-species and private locations are rounded or redacted."],
["Voucher links", "Sample accessions and dataset DOIs must resolve before graph edges publish."],
["Public safety", "Unsafe sample recommendations are held for curator review."]
];

function roundRect(x, y, w, h, r) {
ctx.beginPath();
ctx.moveTo(x + r, y);
ctx.arcTo(x + w, y, x + w, y + h, r);
ctx.arcTo(x + w, y + h, x, y + h, r);
ctx.arcTo(x, y + h, x, y, r);
ctx.arcTo(x, y, x + w, y, r);
ctx.closePath();
}

function draw(frame) {
const t = frame / 52;
ctx.fillStyle = "#f8fafc";
ctx.fillRect(0, 0, 960, 540);
ctx.fillStyle = "#111827";
ctx.font = "bold 34px Arial";
ctx.fillText("Geospatial Sample Provenance Guard", 48, 64);
ctx.font = "19px Arial";
ctx.fillStyle = "#475569";
ctx.fillText("SCIBASE #17 scientific knowledge graph location-edge demo", 48, 98);

ctx.fillStyle = "#e5e7eb";
roundRect(48, 126, 864, 30, 8);
ctx.fill();
ctx.fillStyle = "#0891b2";
roundRect(48, 126, 864 * Math.min(1, t), 30, 8);
ctx.fill();

checks.forEach(([title, text], index) => {
const y = 194 + index * 70;
const active = Math.floor(t * 4.4) >= index;
ctx.fillStyle = active ? "#ffffff" : "#eef2f7";
ctx.strokeStyle = active ? "#0891b2" : "#cbd5e1";
ctx.lineWidth = active ? 3 : 1;
roundRect(48, y, 864, 54, 8);
ctx.fill();
ctx.stroke();
ctx.fillStyle = active ? "#0e7490" : "#64748b";
ctx.font = "bold 18px Arial";
ctx.fillText(title, 70, y + 22);
ctx.fillStyle = "#334155";
ctx.font = "16px Arial";
ctx.fillText(text, 70, y + 44);
});

ctx.fillStyle = "#64748b";
ctx.font = "15px Arial";
ctx.fillText("Synthetic data only. No geocoder, repository, GIS, ontology, specimen, journal, or external API calls.", 48, 504);
}

async function main() {
if (!window.MediaRecorder) {
out.textContent = "ERROR: MediaRecorder unavailable";
return;
}
draw(0);
const stream = canvas.captureStream(12);
const recorder = new MediaRecorder(stream, { mimeType: "video/webm;codecs=vp8" });
const chunks = [];
recorder.ondataavailable = (event) => {
if (event.data && event.data.size > 0) {
chunks.push(event.data);
}
};
recorder.onstop = () => {
const blob = new Blob(chunks, { type: "video/webm" });
const reader = new FileReader();
reader.onloadend = () => {
out.textContent = reader.result;
};
reader.readAsDataURL(blob);
};
recorder.start();
let frame = 0;
const timer = setInterval(() => {
draw(frame);
frame += 1;
if (frame >= 52) {
clearInterval(timer);
recorder.stop();
stream.getTracks().forEach((track) => track.stop());
}
}, 83);
}

main();
</script>
</body>
</html>`;

fs.mkdirSync(reportDir, { recursive: true });

const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "geospatial-provenance-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)}`);
18 changes: 18 additions & 0 deletions geospatial-sample-provenance-guard/demo.js
Original file line number Diff line number Diff line change
@@ -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}`);
Loading