diff --git a/scientific-bounty-review-integrity/demo.js b/scientific-bounty-review-integrity/demo.js new file mode 100644 index 00000000..6de7d559 --- /dev/null +++ b/scientific-bounty-review-integrity/demo.js @@ -0,0 +1,25 @@ +const ScientificBountyReviewIntegrity = require('./index.js'); + +const integrity = new ScientificBountyReviewIntegrity({ threshold: 0.7 }); + +const submission = { + id: 'bounty_108_demo', + title: 'Scientific Bounty Review Integrity Module', + technical_merit: 0.95, + reproducibility: 0.90, + reviewer_affiliation: 'Independent' +}; + +const reviewers = [ + { name: 'Dr. Smith', reviewer_affiliation: 'University A' }, + { name: 'Dr. Jones', reviewer_affiliation: 'University B' }, + { name: 'Dr. Brown', reviewer_affiliation: 'Independent' } +]; + +const rubric = [ + { key: 'technical_merit', weight: 0.6 }, + { key: 'reproducibility', weight: 0.4 } +]; + +const decision = integrity.generateDecision(submission, reviewers, rubric, 1000); +console.log(JSON.stringify(decision, null, 2)); diff --git a/scientific-bounty-review-integrity/index.js b/scientific-bounty-review-integrity/index.js new file mode 100644 index 00000000..e21072f9 --- /dev/null +++ b/scientific-bounty-review-integrity/index.js @@ -0,0 +1,94 @@ +const crypto = require('crypto'); + +class ScientificBountyReviewIntegrity { + constructor(config = {}) { + this.threshold = config.threshold || 0.7; + this.conflictFields = config.conflictFields || ['reviewer_affiliation', 'funding_source']; + } + + createEvidenceManifest(artifacts) { + const manifest = { + artifacts: artifacts.map(a => ({ + name: a.name, + sha256: crypto.createHash('sha256').update(JSON.stringify(a)).digest('hex'), + timestamp: new Date().toISOString() + })), + created_at: new Date().toISOString() + }; + manifest.audit_hash = crypto.createHash('sha256') + .update(JSON.stringify(manifest)) + .digest('hex'); + return manifest; + } + + filterConflictedReviewers(reviewers, submission) { + return reviewers.filter(r => { + for (let field of this.conflictFields) { + if (r[field] === submission[field]) return false; + } + return true; + }); + } + + evaluateRubric(submission, rubric) { + let totalScore = 0; + let totalWeight = 0; + for (let criterion of rubric) { + const score = submission[criterion.key] || 0; + const weight = criterion.weight || 1; + totalScore += score * weight; + totalWeight += weight; + } + const normalizedScore = totalScore / totalWeight; + return { + score: normalizedScore, + passes: normalizedScore >= this.threshold, + threshold: this.threshold + }; + } + + calculatePayoutSplits(totalAmount, milestones) { + const splits = []; + let remaining = totalAmount; + for (let i = 0; i < milestones.length; i++) { + const milestone = milestones[i]; + const amount = i === milestones.length - 1 + ? remaining + : Math.floor(totalAmount * milestone.percentage / 100); + splits.push({ + milestone: milestone.name, + percentage: milestone.percentage, + amount: amount, + status: 'pending' + }); + remaining -= amount; + } + return splits; + } + + generateDecision(submission, reviewers, rubric, totalPayout) { + const validReviewers = this.filterConflictedReviewers(reviewers, submission); + const evaluation = this.evaluateRubric(submission, rubric); + const manifest = this.createEvidenceManifest([submission]); + const payouts = this.calculatePayoutSplits(totalPayout, [ + { name: 'phase_1', percentage: 30 }, + { name: 'phase_2', percentage: 40 }, + { name: 'phase_3', percentage: 30 } + ]); + return { + submission_id: submission.id, + evaluation: evaluation, + reviewer_count: validReviewers.length, + conflict_exclusions: reviewers.length - validReviewers.length, + evidence_manifest: manifest, + payout_splits: payouts, + ip_handoff_state: 'pending_acceptance', + blocker_reasons: evaluation.passes ? [] : ['evaluation_threshold_not_met'], + audit_hash: manifest.audit_hash, + decision_timestamp: new Date().toISOString(), + status: evaluation.passes ? 'approved' : 'rejected' + }; + } +} + +module.exports = ScientificBountyReviewIntegrity; diff --git a/scientific-bounty-review-integrity/test.js b/scientific-bounty-review-integrity/test.js new file mode 100644 index 00000000..f0c4e8f5 --- /dev/null +++ b/scientific-bounty-review-integrity/test.js @@ -0,0 +1,83 @@ +const ScientificBountyReviewIntegrity = require('./index.js'); + +const integrity = new ScientificBountyReviewIntegrity({ threshold: 0.7 }); +let passed = 0, failed = 0; + +try { + const manifest = integrity.createEvidenceManifest([ + { name: 'proposal', content: 'test' }, + { name: 'evidence', content: 'test' } + ]); + if (manifest.audit_hash && manifest.artifacts.length === 2) { + console.log('✓ Test 1: Manifest generation passed'); + passed++; + } else { + console.log('✗ Test 1 failed'); + failed++; + } +} catch (e) { + console.log('✗ Test 1 error:', e.message); + failed++; +} + +try { + const reviewers = [ + { name: 'R1', reviewer_affiliation: 'MIT' }, + { name: 'R2', reviewer_affiliation: 'Stanford' }, + { name: 'R3', reviewer_affiliation: 'MIT' } + ]; + const submission = { id: 's1', reviewer_affiliation: 'MIT' }; + const filtered = integrity.filterConflictedReviewers(reviewers, submission); + if (filtered.length === 1 && filtered[0].name === 'R2') { + console.log('✓ Test 2: Conflict filtering passed'); + passed++; + } else { + console.log('✗ Test 2 failed'); + failed++; + } +} catch (e) { + console.log('✗ Test 2 error:', e.message); + failed++; +} + +try { + const rubric = [ + { key: 'technical_merit', weight: 0.5 }, + { key: 'reproducibility', weight: 0.5 } + ]; + const submission = { id: 's1', technical_merit: 0.8, reproducibility: 0.9 }; + const eval = integrity.evaluateRubric(submission, rubric); + if (eval.score === 0.85 && eval.passes === true) { + console.log('✓ Test 3: Rubric evaluation passed'); + passed++; + } else { + console.log('✗ Test 3 failed'); + failed++; + } +} catch (e) { + console.log('✗ Test 3 error:', e.message); + failed++; +} + +try { + const milestones = [ + { name: 'phase_1', percentage: 30 }, + { name: 'phase_2', percentage: 40 }, + { name: 'phase_3', percentage: 30 } + ]; + const splits = integrity.calculatePayoutSplits(1000, milestones); + const total = splits.reduce((sum, s) => sum + s.amount, 0); + if (total === 1000 && splits.length === 3) { + console.log('✓ Test 4: Payout splits passed'); + passed++; + } else { + console.log('✗ Test 4 failed'); + failed++; + } +} catch (e) { + console.log('✗ Test 4 error:', e.message); + failed++; +} + +console.log(`\nTest Results: ${passed} passed, ${failed} failed`); +process.exit(failed === 0 ? 0 : 1);