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
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,6 @@
# deepevents.ai
deepevents.ai main codebase

## Bounty modules

- [FAIR Artifact Access Gate](fair-artifact-access-gate/README.md) - validates scientific data and code hosting readiness for SCIBASE issue #14.
28 changes: 28 additions & 0 deletions fair-artifact-access-gate/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# FAIR Artifact Access Gate

This module is a focused implementation slice for SCIBASE issue #14, "Scientific/Engineering Data & Code Hosting." It models the pre-publication gate a hosted scientific project should pass before SCIBASE exposes datasets, notebooks, source code, figures, raw instrument outputs, and model files to collaborators, reviewers, or the public.

The gate answers four practical hosting questions:

- Does the project expose machine-readable metadata through JSON-LD, DataCite, and schema.org?
- Are scientific artifacts cataloged by family, access policy, preview capability, versioning, and deterministic hashes?
- Do the hosted materials satisfy FAIR signals for findability, accessibility, interoperability, and reuse?
- Can SCIBASE produce a reviewer export packet that includes metadata, artifact links, access-policy evidence, and checksums?

## Files

- `src/fair-artifact-access-gate.js` contains the deterministic assessment logic.
- `data/sample-artifacts.json` provides a realistic mixed scientific artifact set.
- `test/fair-artifact-access-gate.test.js` covers success and failure paths.
- `scripts/demo.js` prints a concise readiness report for the sample project.
- `docs/demo.svg` and `docs/demo.mp4` show the intended product surface.

## Run

```sh
npm test
npm run demo
npm run demo:video
```

The implementation is intentionally dependency-free so it can be reviewed without installing a framework or service runtime.
107 changes: 107 additions & 0 deletions fair-artifact-access-gate/data/sample-artifacts.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
{
"projectId": "lab-astrocyte-calcium-atlas",
"title": "Astrocyte Calcium Imaging Atlas",
"doi": "10.5555/scibase.astrocyte-calcium.2026.v2",
"version": "2.1.0",
"license": "CC-BY-4.0",
"indexed": true,
"publisher": "SCIBASE Demo Repository",
"persistentBaseUrl": "https://scibase.ai/artifacts/lab-astrocyte-calcium-atlas",
"metadata": {
"jsonLd": {
"@context": "https://schema.org",
"@type": "Dataset",
"name": "Astrocyte Calcium Imaging Atlas",
"identifier": "10.5555/scibase.astrocyte-calcium.2026.v2"
},
"dataCite": {
"identifier": "10.5555/scibase.astrocyte-calcium.2026.v2",
"creators": ["SCIBASE Demo Lab"],
"publisher": "SCIBASE Demo Repository",
"publicationYear": 2026,
"resourceType": "Dataset"
},
"schemaOrg": {
"name": "Astrocyte Calcium Imaging Atlas",
"description": "Processed calcium imaging events, notebooks, source code, model weights, and raw instrument outputs.",
"license": "https://creativecommons.org/licenses/by/4.0/",
"distribution": "https://scibase.ai/artifacts/lab-astrocyte-calcium-atlas"
}
},
"tags": {
"keywords": ["calcium imaging", "astrocyte", "time series", "neuroscience"],
"instruments": ["two-photon microscope", "scientific CMOS camera"],
"organisms": ["Mus musculus"],
"variables": ["event amplitude", "event duration", "field of view", "stimulus epoch"]
},
"artifacts": [
{
"path": "data/processed/calcium-events.parquet",
"family": "dataset",
"format": "parquet",
"access": "public",
"preview": "table",
"versioned": true,
"diffable": true,
"machineReadable": true,
"sizeBytes": 9420800
},
{
"path": "code/notebooks/event-detection.ipynb",
"family": "notebook",
"format": "ipynb",
"access": "public",
"preview": "notebook",
"versioned": true,
"diffable": true,
"machineReadable": true,
"sizeBytes": 184220
},
{
"path": "code/src/extract_events.py",
"family": "code",
"format": "python",
"access": "public",
"preview": "syntax-highlight",
"versioned": true,
"diffable": true,
"machineReadable": true,
"sizeBytes": 38291
},
{
"path": "supplementary/figures/field-of-view.png",
"family": "figure",
"format": "png",
"access": "public",
"preview": "image",
"versioned": true,
"diffable": false,
"machineReadable": false,
"sizeBytes": 308204
},
{
"path": "raw/instrument/session-0423.tiff",
"family": "raw-instrument-output",
"format": "tiff",
"access": "restricted",
"restrictionReason": "contains pre-release animal cohort metadata",
"reviewerAccess": true,
"preview": "thumbnail",
"versioned": true,
"diffable": false,
"machineReadable": false,
"sizeBytes": 182002284
},
{
"path": "models/event-classifier.onnx",
"family": "model",
"format": "onnx",
"access": "public",
"preview": "model-card",
"versioned": true,
"diffable": false,
"machineReadable": true,
"sizeBytes": 14082112
}
]
}
Binary file added fair-artifact-access-gate/docs/demo.mp4
Binary file not shown.
33 changes: 33 additions & 0 deletions fair-artifact-access-gate/docs/demo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
13 changes: 13 additions & 0 deletions fair-artifact-access-gate/docs/requirement-map.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Requirement Map

Issue #14 asks for a platform to host scientific and engineering data plus code. This implementation focuses on the acceptance gate that decides whether a hosted project is ready to expose those artifacts.

| Issue need | Implementation surface |
| --- | --- |
| Store datasets, code, figures, notebooks, raw outputs, and model files | `buildArtifactCatalog` normalizes each artifact family with persistent links, access policy, preview state, versioning, and hashes. |
| Support scientific discovery and citation | `buildMetadataStandardsIndex` checks JSON-LD, DataCite, and schema.org metadata. |
| Keep restricted materials reviewable | `evaluateFairSignals` blocks restricted artifacts without reviewer access and a restriction reason. |
| Produce evidence for maintainers, reviewers, and collaborators | `planReviewerExportPacket` creates a deterministic export manifest with metadata, policy, FAIR report, artifact hashes, and audit digest. |
| Make regressions obvious | `test/fair-artifact-access-gate.test.js` covers the ready path plus metadata, access, and reuse failures. |

The slice is intentionally narrow: it does not build object storage, upload UI, billing, or auth. It defines the deterministic hosting contract those surfaces can call before publication.
15 changes: 15 additions & 0 deletions fair-artifact-access-gate/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"name": "fair-artifact-access-gate",
"version": "0.1.0",
"private": true,
"description": "FAIR scientific artifact hosting readiness gate for SCIBASE issue #14.",
"type": "module",
"scripts": {
"test": "node test/fair-artifact-access-gate.test.js",
"demo": "node scripts/demo.js",
"demo:video": "clang -fobjc-arc -framework Foundation -framework AppKit -framework AVFoundation -framework CoreMedia -framework CoreVideo scripts/render-demo-video.m -o /tmp/scibase-fair-artifact-access-demo && /tmp/scibase-fair-artifact-access-demo docs/demo.mp4"
},
"engines": {
"node": ">=18"
}
}
24 changes: 24 additions & 0 deletions fair-artifact-access-gate/scripts/demo.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { readFile } from "node:fs/promises";
import { fileURLToPath } from "node:url";
import { dirname, join } from "node:path";
import { assessFairArtifactAccessGate } from "../src/fair-artifact-access-gate.js";

const here = dirname(fileURLToPath(import.meta.url));
const samplePath = join(here, "..", "data", "sample-artifacts.json");
const sample = JSON.parse(await readFile(samplePath, "utf8"));
const report = assessFairArtifactAccessGate(sample);

console.log(`Project: ${sample.title}`);
console.log(`Ready: ${report.ready ? "yes" : "no"}`);
console.log(`Artifact families: ${report.catalog.families.join(", ")}`);
console.log(
`FAIR: ${report.fairSignals.signals
.map((signal) => `${signal.key}=${signal.ready ? "pass" : "fail"}`)
.join(", ")}`
);
console.log(`Reviewer packet entries: ${report.reviewerPacket.entryCount}`);
console.log(`Reviewer packet hash: ${report.reviewerPacket.packetHash}`);
console.log(
`Audit digest: standards=${report.reviewerPacket.auditDigest.standardsReady}, fair=${report.reviewerPacket.auditDigest.fairReady}, restricted=${report.reviewerPacket.auditDigest.restrictedArtifactCount}`
);
console.log(`Blockers: ${report.blockers.length === 0 ? "none" : report.blockers.join("; ")}`);
156 changes: 156 additions & 0 deletions fair-artifact-access-gate/scripts/render-demo-video.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
#import <Foundation/Foundation.h>
#import <AppKit/AppKit.h>
#import <AVFoundation/AVFoundation.h>
#import <CoreVideo/CoreVideo.h>

static void FillRounded(NSRect rect, CGFloat radius, NSColor *color) {
NSBezierPath *path = [NSBezierPath bezierPathWithRoundedRect:rect xRadius:radius yRadius:radius];
[color setFill];
[path fill];
}

static void StrokeRounded(NSRect rect, CGFloat radius, NSColor *color) {
NSBezierPath *path = [NSBezierPath bezierPathWithRoundedRect:rect xRadius:radius yRadius:radius];
[color setStroke];
[path setLineWidth:2.0];
[path stroke];
}

static void DrawText(NSString *text, NSRect rect, CGFloat size, NSColor *color, NSFontWeight weight) {
NSMutableParagraphStyle *style = [[NSMutableParagraphStyle alloc] init];
style.lineBreakMode = NSLineBreakByWordWrapping;
NSDictionary *attrs = @{
NSFontAttributeName: [NSFont systemFontOfSize:size weight:weight],
NSForegroundColorAttributeName: color,
NSParagraphStyleAttributeName: style
};
[text drawInRect:rect withAttributes:attrs];
}

static NSColor *RGB(CGFloat r, CGFloat g, CGFloat b) {
return [NSColor colorWithCalibratedRed:r / 255.0 green:g / 255.0 blue:b / 255.0 alpha:1.0];
}

static void DrawCard(NSString *title, NSArray<NSString *> *lines, NSRect rect, NSColor *fill, NSColor *stroke, NSColor *titleColor, NSColor *bodyColor) {
FillRounded(rect, 14.0, fill);
StrokeRounded(rect, 14.0, stroke);
DrawText(title, NSMakeRect(rect.origin.x + 22, rect.origin.y + rect.size.height - 56, rect.size.width - 44, 30), 22, titleColor, NSFontWeightBold);
for (NSUInteger i = 0; i < lines.count; i++) {
DrawText(lines[i], NSMakeRect(rect.origin.x + 22, rect.origin.y + rect.size.height - 96 - (CGFloat)i * 31, rect.size.width - 44, 28), 18, bodyColor, NSFontWeightRegular);
}
}

static void DrawFrame(CGContextRef cg, int width, int height, int frame) {
[NSGraphicsContext saveGraphicsState];
NSGraphicsContext *context = [NSGraphicsContext graphicsContextWithCGContext:cg flipped:NO];
[NSGraphicsContext setCurrentContext:context];

[[NSColor colorWithCalibratedRed:245.0 / 255.0 green:247.0 / 255.0 blue:249.0 / 255.0 alpha:1.0] setFill];
NSRectFill(NSMakeRect(0, 0, width, height));

FillRounded(NSMakeRect(64, 54, 1152, 612), 18, [NSColor whiteColor]);
StrokeRounded(NSMakeRect(64, 54, 1152, 612), 18, RGB(207, 216, 223));

CGFloat progress = MIN(1.0, MAX(0.0, ((CGFloat)frame - 8.0) / 64.0));
DrawText(@"FAIR Artifact Access Gate", NSMakeRect(104, 595, 760, 48), 36, RGB(23, 33, 43), NSFontWeightBold);
DrawText(@"Scientific data and code hosting readiness for SCIBASE issue #14", NSMakeRect(104, 563, 820, 28), 18, RGB(82, 99, 113), NSFontWeightRegular);

DrawCard(@"Metadata Standards", @[@"JSON-LD", @"DataCite", @"schema.org"], NSMakeRect(104, 362, 252, 152), RGB(233, 246, 240), RGB(159, 208, 186), RGB(23, 72, 49), RGB(39, 99, 74));
DrawCard(@"FAIR Signals", @[@"Findable", @"Accessible", @"Reusable"], NSMakeRect(386, 362, 252, 152), RGB(237, 242, 251), RGB(171, 192, 231), RGB(32, 60, 105), RGB(49, 85, 143));
DrawCard(@"Access Policy", @[@"Public links", @"Reviewer URLs", @"Restriction reasons"], NSMakeRect(668, 362, 252, 152), RGB(255, 246, 223), RGB(228, 195, 109), RGB(106, 77, 7), RGB(122, 92, 17));
DrawCard(@"Export Packet", @[@"Metadata", @"Manifest", @"Checksums"], NSMakeRect(950, 362, 226, 152), RGB(248, 237, 247), RGB(213, 171, 208), RGB(90, 40, 84), RGB(115, 56, 107));

FillRounded(NSMakeRect(104, 136, 1072, 174), 14, RGB(23, 33, 43));
DrawText(@"Astrocyte Calcium Imaging Atlas", NSMakeRect(132, 250, 700, 34), 24, [NSColor whiteColor], NSFontWeightBold);
DrawText(@"6 artifact families: dataset, notebook, code, figure, raw instrument output, model", NSMakeRect(132, 209, 920, 28), 18, RGB(200, 211, 220), NSFontWeightRegular);
DrawText(@"Ready: standards pass, FAIR pass, restricted reviewer export pass", NSMakeRect(132, 172, 820, 28), 18, RGB(200, 211, 220), NSFontWeightRegular);
DrawText(@"Deterministic packet hash supports reviewer and collaborator handoff", NSMakeRect(132, 135, 820, 28), 18, RGB(200, 211, 220), NSFontWeightRegular);

CGFloat barWidth = 980.0 * progress;
FillRounded(NSMakeRect(132, 93, barWidth, 10), 5, RGB(64, 175, 120));
DrawText(@"reviewer packet ready", NSMakeRect(132 + barWidth + 14, 84, 220, 28), 14, RGB(82, 99, 113), NSFontWeightMedium);

[NSGraphicsContext restoreGraphicsState];
}

int main(int argc, const char *argv[]) {
@autoreleasepool {
if (argc < 2) {
fprintf(stderr, "usage: render-demo-video output.mp4\n");
return 2;
}

NSString *outputPath = [NSString stringWithUTF8String:argv[1]];
NSURL *outputURL = [NSURL fileURLWithPath:outputPath];
[[NSFileManager defaultManager] removeItemAtURL:outputURL error:nil];

const int width = 1280;
const int height = 720;
NSError *error = nil;
AVAssetWriter *writer = [[AVAssetWriter alloc] initWithURL:outputURL fileType:AVFileTypeMPEG4 error:&error];
if (!writer) {
NSLog(@"writer error: %@", error);
return 1;
}

NSDictionary *videoSettings = @{
AVVideoCodecKey: AVVideoCodecTypeH264,
AVVideoWidthKey: @(width),
AVVideoHeightKey: @(height)
};
AVAssetWriterInput *input = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeVideo outputSettings:videoSettings];
input.expectsMediaDataInRealTime = NO;

NSDictionary *pixelBufferAttributes = @{
(NSString *)kCVPixelBufferPixelFormatTypeKey: @(kCVPixelFormatType_32ARGB),
(NSString *)kCVPixelBufferWidthKey: @(width),
(NSString *)kCVPixelBufferHeightKey: @(height)
};
AVAssetWriterInputPixelBufferAdaptor *adaptor = [AVAssetWriterInputPixelBufferAdaptor assetWriterInputPixelBufferAdaptorWithAssetWriterInput:input sourcePixelBufferAttributes:pixelBufferAttributes];

if (![writer canAddInput:input]) {
NSLog(@"cannot add writer input");
return 1;
}
[writer addInput:input];
[writer startWriting];
[writer startSessionAtSourceTime:kCMTimeZero];

CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
for (int frame = 0; frame < 96; frame++) {
while (!input.readyForMoreMediaData) {
[NSThread sleepForTimeInterval:0.01];
}

CVPixelBufferRef buffer = NULL;
CVPixelBufferPoolCreatePixelBuffer(NULL, adaptor.pixelBufferPool, &buffer);
CVPixelBufferLockBaseAddress(buffer, 0);
void *baseAddress = CVPixelBufferGetBaseAddress(buffer);
size_t bytesPerRow = CVPixelBufferGetBytesPerRow(buffer);
CGContextRef cg = CGBitmapContextCreate(baseAddress, width, height, 8, bytesPerRow, colorSpace, kCGImageAlphaNoneSkipFirst);
DrawFrame(cg, width, height, frame);
CGContextRelease(cg);
CVPixelBufferUnlockBaseAddress(buffer, 0);

CMTime presentationTime = CMTimeMake(frame, 24);
[adaptor appendPixelBuffer:buffer withPresentationTime:presentationTime];
CVPixelBufferRelease(buffer);
}

CGColorSpaceRelease(colorSpace);
[input markAsFinished];

dispatch_semaphore_t sema = dispatch_semaphore_create(0);
[writer finishWritingWithCompletionHandler:^{
dispatch_semaphore_signal(sema);
}];
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);

if (writer.status != AVAssetWriterStatusCompleted) {
NSLog(@"writer failed: %@", writer.error);
return 1;
}
}

return 0;
}
Loading