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

## Added modules

- `repository-api-export-contract/` validates scientific repository manifests, public REST API coverage, export bundle contents, and Git-compatible CLI workflows for SCIBASE project repositories.
27 changes: 27 additions & 0 deletions repository-api-export-contract/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Repository API Export Contract

This module covers the programmatic access and export-bundle portion of issue #10, "Project Repository & Version Control".

It builds a deterministic contract for a scientific project repository that includes:

- typed repository component manifests for manuscript, data, code, notebooks, results, protocols, and metadata
- content hashes and a repository integrity root for reproducibility checks
- REST API route coverage for GET, POST, and PUT workflows plus export access
- export-bundle entries with manifest, metadata, citation, reproducibility, and component files
- a Git-compatible CLI transcript for advanced lab users
- readiness checks that fail closed when required component, API, or reproducibility evidence is missing

## Run

```bash
npm test
npm run demo
npm run demo:video
```

The demo prints the export readiness decision, bundle hash, API coverage, and CLI workflow for the sample project in `data/sample-project.json`.
The video demo renders `docs/demo.mp4` with a four-step walkthrough of the manifest, API coverage, export bundle, and CLI workflow.

## Requirement Map

See [docs/requirement-map.md](docs/requirement-map.md) for the issue requirement mapping.
135 changes: 135 additions & 0 deletions repository-api-export-contract/data/sample-project.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
{
"repositoryId": "neuro-lab/sleep-spindle-atlas",
"title": "Sleep Spindle Atlas With Reproducible Notebook Outputs",
"semanticVersion": "1.2.0",
"tag": "preprint-v1.2",
"doi": "10.5555/scibase.sleep-spindle-atlas.v1.2",
"citation": "Rivera, M.; Chen, I.; Okafor, T. (2026). Sleep Spindle Atlas With Reproducible Notebook Outputs. SCIBASE.AI.",
"authors": [
{
"name": "Mira Rivera",
"orcid": "0000-0002-1000-0001",
"affiliation": "North Coast Neuroscience Lab"
},
{
"name": "Isaac Chen",
"orcid": "0000-0002-1000-0002",
"affiliation": "North Coast Neuroscience Lab"
}
],
"funding": [
{
"funder": "Open Sleep Foundation",
"grant": "OSF-2026-17"
}
],
"metadata": {
"schemaOrgType": "Dataset",
"license": "CC-BY-4.0",
"keywords": ["sleep", "eeg", "spindles", "reproducibility"]
},
"reproducibility": {
"pipeline": "notebooks/run_analysis.ipynb",
"environment": "code/environment.yml",
"status": "passing",
"evidence": [
"results/spindle-summary.csv",
"results/figures/spindle-density.png"
]
},
"components": [
{
"path": "manuscript/main.md",
"mediaType": "text/markdown",
"content": "# Sleep Spindle Atlas\n\nWe report a reproducible spindle atlas across 120 EEG sessions.",
"reproducibilityRole": "narrative"
},
{
"path": "data/eeg-session-index.csv",
"mediaType": "text/csv",
"content": "session_id,subject_id,minutes\ns001,p001,45\ns002,p002,42\n",
"reproducibilityRole": "input-data"
},
{
"path": "code/spindle_detector.py",
"mediaType": "text/x-python",
"content": "def detect_spindles(signal):\n return [window for window in signal if window.get('sigma_power', 0) > 0.72]\n",
"reproducibilityRole": "analysis-code"
},
{
"path": "code/environment.yml",
"mediaType": "application/x-yaml",
"content": "name: spindle-atlas\nchannels: [conda-forge]\ndependencies: [python=3.12, numpy, pandas]\n",
"reproducibilityRole": "environment"
},
{
"path": "notebooks/run_analysis.ipynb",
"mediaType": "application/x-ipynb+json",
"content": {
"cells": [
{
"cell_type": "code",
"source": "from code.spindle_detector import detect_spindles"
}
]
},
"reproducibilityRole": "notebook"
},
{
"path": "results/spindle-summary.csv",
"mediaType": "text/csv",
"content": "group,mean_density\ncontrol,2.4\npatient,1.9\n",
"reproducibilityRole": "reported-output"
},
{
"path": "results/figures/spindle-density.png",
"mediaType": "image/png",
"summary": "PNG chart of spindle density by group",
"sizeBytes": 614400,
"reproducibilityRole": "reported-output"
},
{
"path": "protocols/eeg-acquisition.md",
"mediaType": "text/markdown",
"content": "# EEG Acquisition Protocol\n\nRecord overnight EEG with a 256 Hz sampling rate.",
"reproducibilityRole": "protocol"
},
{
"path": "metadata.json",
"mediaType": "application/json",
"content": {
"@type": "Dataset",
"name": "Sleep Spindle Atlas With Reproducible Notebook Outputs",
"license": "CC-BY-4.0"
},
"reproducibilityRole": "metadata"
}
],
"apiRoutes": [
{
"method": "GET",
"path": "/api/projects/:repositoryId",
"scope": "project.read",
"public": true
},
{
"method": "POST",
"path": "/api/projects",
"scope": "project.write",
"public": true
},
{
"method": "PUT",
"path": "/api/projects/:repositoryId",
"scope": "project.update",
"public": true
},
{
"method": "GET",
"path": "/api/projects/:repositoryId/export",
"scope": "export.read",
"public": true,
"includesIntegrityRoot": true
}
]
}
Binary file added repository-api-export-contract/docs/demo.mp4
Binary file not shown.
27 changes: 27 additions & 0 deletions repository-api-export-contract/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.
15 changes: 15 additions & 0 deletions repository-api-export-contract/docs/requirement-map.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Requirement Map

Issue #10 asks for a scientific project repository system with version control, programmatic access, and export support. This module focuses on the API/export slice so it does not duplicate broad repository ledger, dataset diff, release embargo, schema migration, notebook replay, or citation impact submissions already open.

| Issue requirement | Module coverage |
| --- | --- |
| Repository structure with manuscript, data, code, notebooks, results, protocols, and metadata | `buildRepositoryManifest` validates required component coverage and records typed component entries. |
| Hash-based integrity for reproducibility | Every component receives a deterministic `sha256:` content hash, and the manifest receives an integrity root. |
| Semantic versioning and tagged releases | The manifest records `semanticVersion` and `tag`, and the CLI transcript clones/export by tag. |
| Computation-aware reproducibility | The readiness gate checks reproducibility status and evidence paths before release. |
| DOI and citation generation | Export bundles include `citation/cite-this-project.json` with DOI, citation text, and tag metadata. |
| Public REST API for project and data access | `validateRestApiPlan` requires public GET, POST, and PUT routes with scopes. |
| Export bundles | `planExportBundle` emits manifest, API contract, reproducibility runbook, citation metadata, and component entries. |
| Git-compatible CLI for advanced contributors | `buildGitCompatibleCliPlan` emits clone, status, export, and route-discovery commands. |
| Reviewable local validation | `npm test` covers ready, missing metadata, incomplete API, and failing reproducibility cases. |
11 changes: 11 additions & 0 deletions repository-api-export-contract/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"name": "repository-api-export-contract",
"version": "1.0.0",
"private": true,
"type": "module",
"scripts": {
"test": "node test/repository-api-export-contract.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-repository-api-export-demo && /tmp/scibase-repository-api-export-demo docs/demo.mp4"
}
}
23 changes: 23 additions & 0 deletions repository-api-export-contract/scripts/demo.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { readFileSync } from "node:fs"
import { fileURLToPath } from "node:url"
import { dirname, join } from "node:path"
import { createRepositoryApiExportContract } from "../src/repository-api-export-contract.js"

const here = dirname(fileURLToPath(import.meta.url))
const samplePath = join(here, "../data/sample-project.json")
const project = JSON.parse(readFileSync(samplePath, "utf8"))
const contract = createRepositoryApiExportContract(project)

console.log("Repository API export contract demo")
console.log("Repository:", contract.manifest.repositoryId)
console.log("Tag:", contract.manifest.tag)
console.log("Ready:", contract.readiness.ready)
console.log("Integrity root:", contract.manifest.integrityRoot)
console.log("Bundle hash:", contract.exportBundle.bundleHash)
console.log("API methods:", contract.apiPlan.methods.join(", "))
console.log("Export route:", contract.apiPlan.exportRoute)
console.log("Bundle entries:", contract.exportBundle.entryCount)
console.log("CLI workflow:")
for (const step of contract.cliPlan) {
console.log(`- ${step.command}`)
}
131 changes: 131 additions & 0 deletions repository-api-export-contract/scripts/render-demo-video.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
#import <AVFoundation/AVFoundation.h>
#import <AppKit/AppKit.h>

static NSDictionary *textAttrs(CGFloat size, NSColor *color, BOOL bold) {
NSFont *font = bold ? [NSFont boldSystemFontOfSize:size] : [NSFont systemFontOfSize:size];
return @{NSFontAttributeName: font, NSForegroundColorAttributeName: color};
}

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

static void drawText(NSString *text, CGFloat x, CGFloat y, CGFloat width, CGFloat size, NSColor *color, BOOL bold) {
[text drawInRect:NSMakeRect(x, y, width, size + 12.0) withAttributes:textAttrs(size, color, bold)];
}

static void drawFrame(CGContextRef context, int frame, int totalFrames) {
CGFloat t = (CGFloat)frame / (CGFloat)(totalFrames - 1);
int stage = MIN(3, (int)floor(t * 4.0));

NSGraphicsContext *graphicsContext = [NSGraphicsContext graphicsContextWithCGContext:context flipped:NO];
[NSGraphicsContext saveGraphicsState];
[NSGraphicsContext setCurrentContext:graphicsContext];

[[NSColor colorWithCalibratedRed:0.06 green:0.09 blue:0.16 alpha:1.0] setFill];
NSRectFill(NSMakeRect(0, 0, 1280, 720));
fillRound(NSMakeRect(64, 56, 1152, 608), 12, [NSColor colorWithCalibratedWhite:0.98 alpha:1.0], nil);

drawText(@"Repository API Export Contract", 104, 594, 900, 34, [NSColor colorWithCalibratedWhite:0.07 alpha:1.0], YES);
drawText(@"Programmatic access and export-bundle readiness for SCIBASE project repositories", 104, 558, 960, 18, [NSColor colorWithCalibratedRed:0.28 green:0.33 blue:0.41 alpha:1.0], NO);

NSArray *cards = @[
@{@"title": @"1. Manifest", @"body": @"Required repository components and deterministic content hashes", @"detail": @"manuscript, data, code, notebooks, results, protocols, metadata", @"rect": [NSValue valueWithRect:NSMakeRect(104, 366, 500, 150)], @"fill": @[@0.88, @0.96, @0.99], @"stroke": @[@0.01, @0.52, @0.78]},
@{@"title": @"2. REST API", @"body": @"Public GET, POST, and PUT routes with scoped access", @"detail": @"export route carries the manifest integrity root", @"rect": [NSValue valueWithRect:NSMakeRect(676, 366, 500, 150)], @"fill": @[@0.86, @0.99, @0.91], @"stroke": @[@0.09, @0.64, @0.29]},
@{@"title": @"3. Export Bundle", @"body": @"Manifest, API contract, runbook, citation metadata, and files", @"detail": @"ordered entries sign a reproducible bundle hash", @"rect": [NSValue valueWithRect:NSMakeRect(104, 160, 500, 150)], @"fill": @[@1.0, @0.95, @0.78], @"stroke": @[@0.85, @0.47, @0.02]},
@{@"title": @"4. CLI Workflow", @"body": @"clone, status, export, and route discovery commands", @"detail": @"Git-compatible automation for lab users", @"rect": [NSValue valueWithRect:NSMakeRect(676, 160, 500, 150)], @"fill": @[@0.95, @0.91, @1.0], @"stroke": @[@0.58, @0.20, @0.92]},
];

for (NSUInteger i = 0; i < [cards count]; i++) {
NSDictionary *card = cards[i];
NSRect rect = [card[@"rect"] rectValue];
NSArray *fill = card[@"fill"];
NSArray *stroke = card[@"stroke"];
NSColor *fillColor = [NSColor colorWithCalibratedRed:[fill[0] doubleValue] green:[fill[1] doubleValue] blue:[fill[2] doubleValue] alpha:1.0];
NSColor *strokeColor = [NSColor colorWithCalibratedRed:[stroke[0] doubleValue] green:[stroke[1] doubleValue] blue:[stroke[2] doubleValue] alpha:1.0];
fillRound(rect, 10, fillColor, strokeColor);
if ((int)i == stage) {
NSBezierPath *highlight = [NSBezierPath bezierPathWithRoundedRect:NSInsetRect(rect, -8, -8) xRadius:14 yRadius:14];
[[NSColor colorWithCalibratedRed:0.10 green:0.45 blue:0.90 alpha:0.28] setFill];
[highlight fill];
}
drawText(card[@"title"], rect.origin.x + 28, rect.origin.y + 108, 420, 24, strokeColor, YES);
drawText(card[@"body"], rect.origin.x + 28, rect.origin.y + 70, 430, 17, [NSColor colorWithCalibratedWhite:0.08 alpha:1.0], NO);
drawText(card[@"detail"], rect.origin.x + 28, rect.origin.y + 38, 430, 15, [NSColor colorWithCalibratedRed:0.30 green:0.35 blue:0.43 alpha:1.0], NO);
}

NSString *footer = [NSString stringWithFormat:@"ready=true entries=13 api=GET,POST,PUT bundle=sha256:cfa0be77...818c frame=%d/%d", frame + 1, totalFrames];
drawText(footer, 104, 90, 1000, 18, [NSColor colorWithCalibratedRed:0.20 green:0.25 blue:0.33 alpha:1.0], NO);

[NSGraphicsContext restoreGraphicsState];
}

int main(int argc, const char *argv[]) {
@autoreleasepool {
NSString *output = argc > 1 ? [NSString stringWithUTF8String:argv[1]] : @"docs/demo.mp4";
NSURL *outputURL = [NSURL fileURLWithPath:output];
[[NSFileManager defaultManager] removeItemAtURL:outputURL error:nil];

NSError *error = nil;
AVAssetWriter *writer = [[AVAssetWriter alloc] initWithURL:outputURL fileType:AVFileTypeMPEG4 error:&error];
if (!writer) {
NSLog(@"failed to create writer: %@", error);
return 1;
}

NSDictionary *settings = @{
AVVideoCodecKey: AVVideoCodecTypeH264,
AVVideoWidthKey: @1280,
AVVideoHeightKey: @720,
AVVideoCompressionPropertiesKey: @{AVVideoAverageBitRateKey: @2500000}
};
AVAssetWriterInput *input = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeVideo outputSettings:settings];
input.expectsMediaDataInRealTime = NO;
NSDictionary *attributes = @{
(NSString *)kCVPixelBufferPixelFormatTypeKey: @(kCVPixelFormatType_32ARGB),
(NSString *)kCVPixelBufferWidthKey: @1280,
(NSString *)kCVPixelBufferHeightKey: @720,
};
AVAssetWriterInputPixelBufferAdaptor *adaptor = [AVAssetWriterInputPixelBufferAdaptor assetWriterInputPixelBufferAdaptorWithAssetWriterInput:input sourcePixelBufferAttributes:attributes];
[writer addInput:input];
[writer startWriting];
[writer startSessionAtSourceTime:kCMTimeZero];

const int fps = 24;
const int totalFrames = 96;
for (int frame = 0; frame < totalFrames; frame++) {
while (!input.readyForMoreMediaData) {
[NSThread sleepForTimeInterval:0.01];
}
CVPixelBufferRef pixelBuffer = NULL;
CVPixelBufferPoolCreatePixelBuffer(NULL, adaptor.pixelBufferPool, &pixelBuffer);
CVPixelBufferLockBaseAddress(pixelBuffer, 0);
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
CGContextRef context = CGBitmapContextCreate(CVPixelBufferGetBaseAddress(pixelBuffer), 1280, 720, 8, CVPixelBufferGetBytesPerRow(pixelBuffer), colorSpace, kCGImageAlphaPremultipliedFirst);
drawFrame(context, frame, totalFrames);
CGContextRelease(context);
CGColorSpaceRelease(colorSpace);
CVPixelBufferUnlockBaseAddress(pixelBuffer, 0);
[adaptor appendPixelBuffer:pixelBuffer withPresentationTime:CMTimeMake(frame, fps)];
CVPixelBufferRelease(pixelBuffer);
}

[input markAsFinished];
[writer finishWritingWithCompletionHandler:^{}];
while (writer.status == AVAssetWriterStatusWriting) {
[NSThread sleepForTimeInterval:0.05];
}
if (writer.status != AVAssetWriterStatusCompleted) {
NSLog(@"writer failed: %@", writer.error);
return 1;
}
}
return 0;
}
Loading