From 11f6fed6228a4e0bd02ded470b1e007056fa1e1f Mon Sep 17 00:00:00 2001 From: attaboy11 Date: Sun, 31 May 2026 06:06:02 +0100 Subject: [PATCH 1/2] Add scientific bounty scope change guard --- .../README.md | 35 ++ scientific-bounty-scope-change-guard/demo.js | 24 ++ scientific-bounty-scope-change-guard/index.js | 383 ++++++++++++++++++ .../render-video.js | 173 ++++++++ .../reports/demo.gif | Bin 0 -> 16442 bytes .../reports/scope-change-review.json | 283 +++++++++++++ .../reports/scope-change-review.md | 65 +++ .../reports/scope-change-review.svg | 9 + .../sample-data.js | 126 ++++++ scientific-bounty-scope-change-guard/test.js | 84 ++++ 10 files changed, 1182 insertions(+) create mode 100644 scientific-bounty-scope-change-guard/README.md create mode 100644 scientific-bounty-scope-change-guard/demo.js create mode 100644 scientific-bounty-scope-change-guard/index.js create mode 100644 scientific-bounty-scope-change-guard/render-video.js create mode 100644 scientific-bounty-scope-change-guard/reports/demo.gif create mode 100644 scientific-bounty-scope-change-guard/reports/scope-change-review.json create mode 100644 scientific-bounty-scope-change-guard/reports/scope-change-review.md create mode 100644 scientific-bounty-scope-change-guard/reports/scope-change-review.svg create mode 100644 scientific-bounty-scope-change-guard/sample-data.js create mode 100644 scientific-bounty-scope-change-guard/test.js diff --git a/scientific-bounty-scope-change-guard/README.md b/scientific-bounty-scope-change-guard/README.md new file mode 100644 index 00000000..ed7583c8 --- /dev/null +++ b/scientific-bounty-scope-change-guard/README.md @@ -0,0 +1,35 @@ +# Scientific Bounty Scope Change Guard + +Self-contained guard for SCIBASE issue #18, Scientific Bounty System. + +The module reviews sponsor-requested changes after a scientific bounty is launched. It blocks or escalates changes that would move the goalposts for solver teams: problem statements, deliverables, scoring rubrics, eligibility, prize schedules, NDA/data-room terms, and post-submission effective dates. + +## What It Checks + +- Material changes after launch are frozen unless they include sponsor, review, and audit evidence. +- Submitted or in-progress solver work is grandfathered when scope changes after submissions begin. +- Rubric weights still total 100 and changed scoring criteria have equal-notice evidence. +- Eligibility and NDA/data-room changes cannot silently narrow access for active teams. +- Prize schedule changes need finance/escrow evidence before scoring or payout. +- Active solver teams receive equal notice, and material changes require consent or a deadline extension. + +## Files + +- `index.js` - evaluation engine and report formatters +- `sample-data.js` - synthetic challenge scenarios +- `test.js` - dependency-free tests using Node's built-in `assert` +- `demo.js` - generates reviewer JSON, Markdown, and SVG reports +- `render-video.js` - renders a short MP4 with `ffmpeg`, or an animated GIF fallback with ImageMagick + +## Validation + +```bash +node scientific-bounty-scope-change-guard/test.js +node scientific-bounty-scope-change-guard/demo.js +node scientific-bounty-scope-change-guard/render-video.js +node --check scientific-bounty-scope-change-guard/index.js +node --check scientific-bounty-scope-change-guard/sample-data.js +node --check scientific-bounty-scope-change-guard/test.js +node --check scientific-bounty-scope-change-guard/demo.js +node --check scientific-bounty-scope-change-guard/render-video.js +``` diff --git a/scientific-bounty-scope-change-guard/demo.js b/scientific-bounty-scope-change-guard/demo.js new file mode 100644 index 00000000..690d7d43 --- /dev/null +++ b/scientific-bounty-scope-change-guard/demo.js @@ -0,0 +1,24 @@ +const fs = require('fs'); +const path = require('path'); +const { + evaluateScopeChangeControl, + formatMarkdownReport, + formatSvgSummary +} = require('./index'); +const { reviewInput } = require('./sample-data'); + +const reportsDir = path.join(__dirname, 'reports'); +fs.mkdirSync(reportsDir, { recursive: true }); + +const evaluation = evaluateScopeChangeControl(reviewInput); + +fs.writeFileSync( + path.join(reportsDir, 'scope-change-review.json'), + `${JSON.stringify(evaluation, null, 2)}\n` +); +fs.writeFileSync(path.join(reportsDir, 'scope-change-review.md'), formatMarkdownReport(evaluation)); +fs.writeFileSync(path.join(reportsDir, 'scope-change-review.svg'), formatSvgSummary(evaluation)); + +console.log(`Generated reports in ${reportsDir}`); +console.log(`Overall decision: ${evaluation.overallDecision}`); + diff --git a/scientific-bounty-scope-change-guard/index.js b/scientific-bounty-scope-change-guard/index.js new file mode 100644 index 00000000..04217f7b --- /dev/null +++ b/scientific-bounty-scope-change-guard/index.js @@ -0,0 +1,383 @@ +const crypto = require('crypto'); + +const MATERIAL_FIELDS = new Set([ + 'problemStatement', + 'deliverables', + 'rubric', + 'eligibility', + 'prizeSchedule', + 'ndaTerms', + 'dataRoomTerms' +]); + +const FIELD_LABELS = { + problemStatement: 'problem statement', + deliverables: 'deliverables', + rubric: 'scoring rubric', + eligibility: 'solver eligibility', + prizeSchedule: 'prize schedule', + ndaTerms: 'NDA terms', + dataRoomTerms: 'data-room terms' +}; + +const NOTICE_HOURS = 72; + +function stableStringify(value) { + if (Array.isArray(value)) { + return `[${value.map(stableStringify).join(',')}]`; + } + if (value && typeof value === 'object') { + return `{${Object.keys(value) + .sort() + .map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`) + .join(',')}}`; + } + return JSON.stringify(value); +} + +function digest(value) { + return crypto.createHash('sha256').update(stableStringify(value)).digest('hex').slice(0, 16); +} + +function hoursBetween(start, end) { + return (new Date(end).getTime() - new Date(start).getTime()) / 36e5; +} + +function addFinding(findings, severity, code, message, evidence = {}) { + findings.push({ severity, code, message, evidence }); +} + +function changedMaterialFields(request) { + return Object.keys(request.changes || {}).filter((field) => MATERIAL_FIELDS.has(field)); +} + +function rubricWeightTotal(rubric = []) { + return rubric.reduce((sum, item) => sum + Number(item.weight || 0), 0); +} + +function compareRubric(previous = [], next = []) { + const previousById = new Map(previous.map((item) => [item.id, item])); + const nextById = new Map(next.map((item) => [item.id, item])); + const added = []; + const removed = []; + const changed = []; + + for (const [id, item] of nextById) { + if (!previousById.has(id)) { + added.push(id); + continue; + } + const before = previousById.get(id); + if (before.weight !== item.weight || before.maxPoints !== item.maxPoints || before.label !== item.label) { + changed.push({ + id, + before: { label: before.label, weight: before.weight, maxPoints: before.maxPoints }, + after: { label: item.label, weight: item.weight, maxPoints: item.maxPoints } + }); + } + } + + for (const id of previousById.keys()) { + if (!nextById.has(id)) { + removed.push(id); + } + } + + return { + added, + removed, + changed, + beforeTotal: rubricWeightTotal(previous), + afterTotal: rubricWeightTotal(next) + }; +} + +function activeTeams(participants = []) { + return participants.filter((team) => ['registered', 'submitted', 'finalist'].includes(team.status)); +} + +function submittedTeams(participants = []) { + return participants.filter((team) => team.submittedAt); +} + +function notificationGaps(request, participants = []) { + const notified = new Set((request.notice || {}).teamNotices || []); + return activeTeams(participants) + .filter((team) => !notified.has(team.teamId)) + .map((team) => team.teamId); +} + +function consentGaps(request, participants = []) { + const consented = new Set((request.notice || {}).teamConsents || []); + return activeTeams(participants) + .filter((team) => !consented.has(team.teamId)) + .map((team) => team.teamId); +} + +function approvalGaps(request) { + const approvals = request.approvals || {}; + const gaps = []; + if (!approvals.sponsor) gaps.push('sponsor approval'); + if (!approvals.independentReviewer) gaps.push('independent reviewer approval'); + if (!approvals.auditLog) gaps.push('immutable audit log'); + return gaps; +} + +function evaluateChangeRequest(challenge, request) { + const findings = []; + const materialFields = changedMaterialFields(request); + const launchedAt = new Date(challenge.launchedAt); + const requestedAt = new Date(request.requestedAt); + const effectiveAt = new Date(request.effectiveAt || request.requestedAt); + const afterLaunch = requestedAt >= launchedAt; + const submissions = submittedTeams(challenge.participants); + const freezeDigestBefore = digest(challenge.baseline); + const freezeDigestAfter = digest({ ...challenge.baseline, ...request.changes }); + + if (materialFields.length === 0) { + addFinding(findings, 'info', 'NON_MATERIAL_CHANGE', 'Only non-material copy or metadata fields changed.', { + changedFields: Object.keys(request.changes || {}) + }); + } + + if (afterLaunch && materialFields.length > 0) { + addFinding(findings, 'medium', 'MATERIAL_CHANGE_AFTER_LAUNCH', 'Material challenge fields changed after public launch.', { + changedFields: materialFields.map((field) => FIELD_LABELS[field] || field), + requestedAt: request.requestedAt, + launchedAt: challenge.launchedAt + }); + + const gaps = approvalGaps(request); + if (gaps.length > 0) { + addFinding(findings, 'critical', 'MISSING_CHANGE_APPROVALS', 'Post-launch material changes need sponsor, reviewer, and audit evidence.', { + missing: gaps + }); + } + } + + const missingNotice = notificationGaps(request, challenge.participants); + if (materialFields.length > 0 && missingNotice.length > 0) { + addFinding(findings, 'critical', 'UNEQUAL_SOLVER_NOTICE', 'Active solver teams were not all notified before the change takes effect.', { + teams: missingNotice + }); + } + + const noticeHours = request.notice && request.notice.sentAt ? hoursBetween(request.notice.sentAt, effectiveAt) : 0; + if (materialFields.length > 0 && noticeHours < NOTICE_HOURS) { + addFinding(findings, 'high', 'SHORT_NOTICE_WINDOW', 'Material changes need at least 72 hours notice or an explicit deadline extension.', { + noticeHours, + minimumHours: NOTICE_HOURS, + deadlineExtensionHours: request.deadlineExtensionHours || 0 + }); + } + + if (request.requiresSolverConsent) { + const missingConsent = consentGaps(request, challenge.participants); + if (missingConsent.length > 0) { + addFinding(findings, 'high', 'MISSING_SOLVER_CONSENT', 'Material scope changes requiring consent are missing active-team acknowledgements.', { + teams: missingConsent + }); + } + } + + if (submissions.length > 0 && materialFields.length > 0 && !request.grandfatherSubmittedWork) { + addFinding(findings, 'critical', 'SUBMITTED_WORK_NOT_GRANDFATHERED', 'Teams that already submitted work must be scored under the original scope or explicitly grandfathered.', { + submittedTeams: submissions.map((team) => team.teamId) + }); + } + + if (request.changes && request.changes.rubric) { + const comparison = compareRubric(challenge.baseline.rubric, request.changes.rubric); + if (comparison.afterTotal !== 100) { + addFinding(findings, 'critical', 'RUBRIC_WEIGHT_TOTAL_INVALID', 'Changed rubric weights must total 100 before scoring.', { + afterTotal: comparison.afterTotal + }); + } + if (comparison.added.length || comparison.removed.length || comparison.changed.length) { + addFinding(findings, 'medium', 'RUBRIC_CRITERIA_CHANGED', 'Rubric criteria changed and must be visible in the reviewer packet.', comparison); + } + } + + if (request.changes && request.changes.eligibility && !request.grandfatherRegisteredTeams) { + addFinding(findings, 'high', 'ELIGIBILITY_NARROWED_WITHOUT_GRANDFATHERING', 'Eligibility changes must not disqualify already registered teams without grandfathering.', { + activeTeams: activeTeams(challenge.participants).map((team) => team.teamId) + }); + } + + if (request.changes && request.changes.prizeSchedule) { + const finance = request.approvals && request.approvals.financeEscrow; + if (!finance) { + addFinding(findings, 'critical', 'PRIZE_CHANGE_WITHOUT_ESCROW_EVIDENCE', 'Prize or payout schedule changes need finance/escrow evidence before scoring proceeds.', { + prizeSchedule: request.changes.prizeSchedule + }); + } + } + + if (request.changes && (request.changes.ndaTerms || request.changes.dataRoomTerms)) { + if (!request.equalDataRoomAccess) { + addFinding(findings, 'high', 'DATA_ROOM_ACCESS_NOT_RECONFIRMED', 'NDA or data-room changes require equal re-access evidence for every active team.', { + changedFields: ['ndaTerms', 'dataRoomTerms'].filter((field) => request.changes[field]) + }); + } + } + + const hasCritical = findings.some((finding) => finding.severity === 'critical'); + const hasHigh = findings.some((finding) => finding.severity === 'high'); + const hasMedium = findings.some((finding) => finding.severity === 'medium'); + const decision = hasCritical || hasHigh ? 'hold' : hasMedium ? 'review' : 'allow'; + + return { + challengeId: challenge.challengeId, + requestId: request.id, + decision, + materialFields, + freezeDigestBefore, + freezeDigestAfter, + findingCounts: countFindings(findings), + findings, + actions: buildActions(decision, findings, request) + }; +} + +function countFindings(findings) { + return findings.reduce((counts, finding) => { + counts[finding.severity] = (counts[finding.severity] || 0) + 1; + return counts; + }, {}); +} + +function buildActions(decision, findings, request) { + if (decision === 'allow') { + return ['Record audit digest', 'Publish change packet', 'Continue normal scoring']; + } + + const actions = new Set(['Pause scoring for affected challenge', 'Publish reviewer-visible change packet']); + for (const finding of findings) { + if (finding.code === 'UNEQUAL_SOLVER_NOTICE') actions.add('Notify every active solver team'); + if (finding.code === 'MISSING_SOLVER_CONSENT') actions.add('Collect consent or extend/reopen the deadline'); + if (finding.code === 'SUBMITTED_WORK_NOT_GRANDFATHERED') actions.add('Grandfather submitted work under original scope'); + if (finding.code === 'MISSING_CHANGE_APPROVALS') actions.add('Attach sponsor, independent reviewer, and immutable audit approvals'); + if (finding.code === 'PRIZE_CHANGE_WITHOUT_ESCROW_EVIDENCE') actions.add('Attach finance escrow evidence'); + if (finding.code === 'DATA_ROOM_ACCESS_NOT_RECONFIRMED') actions.add('Reconfirm equal data-room access'); + } + + if ((request.deadlineExtensionHours || 0) < NOTICE_HOURS) { + actions.add('Add a deadline extension matching the notice window'); + } + + return Array.from(actions); +} + +function evaluateScopeChangeControl(input) { + const results = input.changeRequests.map((request) => evaluateChangeRequest(input.challenge, request)); + const decisionRank = { allow: 0, review: 1, hold: 2 }; + const overallDecision = results.reduce( + (current, result) => (decisionRank[result.decision] > decisionRank[current] ? result.decision : current), + 'allow' + ); + + return { + generatedAt: input.generatedAt || new Date().toISOString(), + challengeId: input.challenge.challengeId, + title: input.challenge.title, + overallDecision, + requestCount: results.length, + results, + reviewerPacket: buildReviewerPacket(input.challenge, results) + }; +} + +function buildReviewerPacket(challenge, results) { + return { + challengeId: challenge.challengeId, + launchedAt: challenge.launchedAt, + baselineDigest: digest(challenge.baseline), + activeTeams: activeTeams(challenge.participants).map((team) => team.teamId), + submittedTeams: submittedTeams(challenge.participants).map((team) => team.teamId), + heldRequests: results.filter((result) => result.decision === 'hold').map((result) => result.requestId), + reviewRequests: results.filter((result) => result.decision === 'review').map((result) => result.requestId), + allowRequests: results.filter((result) => result.decision === 'allow').map((result) => result.requestId) + }; +} + +function formatMarkdownReport(evaluation) { + const lines = [ + `# Scope Change Review: ${evaluation.title}`, + '', + `Overall decision: **${evaluation.overallDecision.toUpperCase()}**`, + `Challenge: \`${evaluation.challengeId}\``, + `Generated: ${evaluation.generatedAt}`, + '', + '## Requests' + ]; + + for (const result of evaluation.results) { + lines.push('', `### ${result.requestId}`, ''); + lines.push(`Decision: **${result.decision.toUpperCase()}**`); + lines.push(`Material fields: ${result.materialFields.length ? result.materialFields.join(', ') : 'none'}`); + lines.push(`Freeze digest before: \`${result.freezeDigestBefore}\``); + lines.push(`Freeze digest after: \`${result.freezeDigestAfter}\``); + lines.push(''); + lines.push('Findings:'); + for (const finding of result.findings) { + lines.push(`- ${finding.severity.toUpperCase()} ${finding.code}: ${finding.message}`); + } + lines.push(''); + lines.push('Actions:'); + for (const action of result.actions) { + lines.push(`- ${action}`); + } + } + + return `${lines.join('\n')}\n`; +} + +function formatSvgSummary(evaluation) { + const colors = { + allow: '#15803d', + review: '#a16207', + hold: '#b91c1c' + }; + const rows = evaluation.results + .map((result, index) => { + const y = 170 + index * 58; + return [ + ``, + `${escapeXml(result.requestId)}`, + `${result.decision.toUpperCase()}`, + `${escapeXml(result.actions[0] || 'Record audit')}` + ].join(''); + }) + .join(''); + + return [ + '', + '', + '', + 'Scientific bounty scope-change guard', + `Overall decision: ${evaluation.overallDecision.toUpperCase()} | Requests: ${evaluation.requestCount}`, + 'Reviewer queue', + rows, + 'Synthetic data only. Blocks goalpost-moving changes before scoring or payout.', + '' + ].join('\n'); +} + +function escapeXml(value) { + return String(value) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"'); +} + +module.exports = { + evaluateScopeChangeControl, + evaluateChangeRequest, + compareRubric, + formatMarkdownReport, + formatSvgSummary, + stableStringify, + digest +}; + diff --git a/scientific-bounty-scope-change-guard/render-video.js b/scientific-bounty-scope-change-guard/render-video.js new file mode 100644 index 00000000..5270b036 --- /dev/null +++ b/scientific-bounty-scope-change-guard/render-video.js @@ -0,0 +1,173 @@ +const path = require('path'); +const fs = require('fs'); +const { spawnSync } = require('child_process'); + +const output = path.join(__dirname, 'reports', 'demo.mp4'); +const gifOutput = path.join(__dirname, 'reports', 'demo.gif'); +const filter = [ + "drawtext=fontcolor=white:fontsize=36:x=48:y=60:text='SCIBASE scientific bounty scope guard'", + "drawtext=fontcolor=white:fontsize=28:x=48:y=140:text='HOLD unsafe post-launch changes'", + "drawtext=fontcolor=0xCBD5E1:fontsize=24:x=48:y=210:text='Checks rubric, deliverables, prize, eligibility, NDA, notices'", + "drawtext=fontcolor=0xCBD5E1:fontsize=24:x=48:y=260:text='Grandfathers submitted work and requires equal solver notice'", + "drawtext=fontcolor=0xFCA5A5:fontsize=30:x=48:y=350:text='Demo output: one hold, one review, one allow'" +].join(','); + +const ffmpegReady = spawnSync('ffmpeg', ['-version'], { stdio: 'ignore' }).status === 0; +const result = ffmpegReady + ? spawnSync( + 'ffmpeg', + [ + '-y', + '-f', + 'lavfi', + '-i', + 'color=c=0f172a:s=960x540:d=5', + '-vf', + filter, + '-pix_fmt', + 'yuv420p', + output + ], + { stdio: 'inherit' } + ) + : { status: 1 }; + +if (result.status === 0) { + console.log(`Rendered ${output}`); + process.exit(0); +} + +console.log('ffmpeg is unavailable; trying ImageMagick GIF fallback.'); +setImmediate(renderGifFallback); + +function renderGifFallback() { + const reportsDir = path.join(__dirname, 'reports'); + fs.mkdirSync(reportsDir, { recursive: true }); + + const frames = [ + { + name: 'frame-01.ppm', + title: 'SCOPE GUARD', + subtitle: 'FREEZE BASELINE', + accent: '#38bdf8' + }, + { + name: 'frame-02.ppm', + title: 'RUBRIC SHIFT', + subtitle: 'PRIZE ELIGIBILITY', + accent: '#f97316' + }, + { + name: 'frame-03.ppm', + title: 'NOTICE TEAMS', + subtitle: 'CONSENT EXTEND', + accent: '#22c55e' + }, + { + name: 'frame-04.ppm', + title: 'HOLD REVIEW', + subtitle: 'ALLOW SAFE COPY', + accent: '#ef4444' + } + ].map((frame) => { + const file = path.join(reportsDir, frame.name); + fs.writeFileSync(file, ppmFrame(frame.title, frame.subtitle, frame.accent)); + return file; + }); + + const magick = spawnSync('magick', ['-delay', '140', '-loop', '0', ...frames, gifOutput], { + stdio: 'inherit' + }); + + if (magick.status !== 0) { + process.exit(magick.status || 1); + } + + for (const frame of frames) { + fs.unlinkSync(frame); + } + + console.log(`Rendered ${gifOutput}`); +} + +function ppmFrame(title, subtitle, accent) { + const width = 480; + const height = 270; + const pixels = Buffer.alloc(width * height * 3); + fill(pixels, width, height, hex('#0f172a')); + rect(pixels, width, height, 24, 28, 10, 196, hex(accent)); + rect(pixels, width, height, 56, 142, 360, 54, hex('#1e293b')); + rect(pixels, width, height, 56, 210, 250, 10, hex(accent)); + drawText(pixels, width, height, title, 56, 58, 6, hex('#f8fafc')); + drawText(pixels, width, height, subtitle, 58, 110, 3, hex('#cbd5e1')); + drawText(pixels, width, height, 'SCIBASE ISSUE 18', 78, 160, 3, hex('#f8fafc')); + drawText(pixels, width, height, 'SYNTHETIC DEMO', 58, 236, 2, hex('#94a3b8')); + return Buffer.concat([Buffer.from(`P6\n${width} ${height}\n255\n`), pixels]); +} + +function fill(pixels, width, height, color) { + rect(pixels, width, height, 0, 0, width, height, color); +} + +function rect(pixels, width, height, x, y, w, h, color) { + for (let row = Math.max(0, y); row < Math.min(height, y + h); row += 1) { + for (let col = Math.max(0, x); col < Math.min(width, x + w); col += 1) { + const offset = (row * width + col) * 3; + pixels[offset] = color[0]; + pixels[offset + 1] = color[1]; + pixels[offset + 2] = color[2]; + } + } +} + +function drawText(pixels, width, height, text, x, y, scale, color) { + let cursor = x; + for (const char of text.toUpperCase()) { + if (char === ' ') { + cursor += 4 * scale; + continue; + } + const glyph = FONT[char] || FONT['?']; + for (let row = 0; row < glyph.length; row += 1) { + for (let col = 0; col < glyph[row].length; col += 1) { + if (glyph[row][col] === '1') { + rect(pixels, width, height, cursor + col * scale, y + row * scale, scale, scale, color); + } + } + } + cursor += 6 * scale; + } +} + +function hex(value) { + const clean = value.replace('#', ''); + return [0, 2, 4].map((index) => parseInt(clean.slice(index, index + 2), 16)); +} + +const FONT = { + A: ['01110', '10001', '10001', '11111', '10001', '10001', '10001'], + B: ['11110', '10001', '10001', '11110', '10001', '10001', '11110'], + C: ['01111', '10000', '10000', '10000', '10000', '10000', '01111'], + D: ['11110', '10001', '10001', '10001', '10001', '10001', '11110'], + E: ['11111', '10000', '10000', '11110', '10000', '10000', '11111'], + F: ['11111', '10000', '10000', '11110', '10000', '10000', '10000'], + G: ['01111', '10000', '10000', '10111', '10001', '10001', '01111'], + H: ['10001', '10001', '10001', '11111', '10001', '10001', '10001'], + I: ['11111', '00100', '00100', '00100', '00100', '00100', '11111'], + L: ['10000', '10000', '10000', '10000', '10000', '10000', '11111'], + M: ['10001', '11011', '10101', '10101', '10001', '10001', '10001'], + N: ['10001', '11001', '10101', '10011', '10001', '10001', '10001'], + O: ['01110', '10001', '10001', '10001', '10001', '10001', '01110'], + P: ['11110', '10001', '10001', '11110', '10000', '10000', '10000'], + R: ['11110', '10001', '10001', '11110', '10100', '10010', '10001'], + S: ['01111', '10000', '10000', '01110', '00001', '00001', '11110'], + T: ['11111', '00100', '00100', '00100', '00100', '00100', '00100'], + U: ['10001', '10001', '10001', '10001', '10001', '10001', '01110'], + V: ['10001', '10001', '10001', '10001', '10001', '01010', '00100'], + W: ['10001', '10001', '10001', '10101', '10101', '10101', '01010'], + Y: ['10001', '10001', '01010', '00100', '00100', '00100', '00100'], + Z: ['11111', '00001', '00010', '00100', '01000', '10000', '11111'], + 1: ['00100', '01100', '00100', '00100', '00100', '00100', '01110'], + 8: ['01110', '10001', '10001', '01110', '10001', '10001', '01110'], + '?': ['11110', '00001', '00010', '00100', '00100', '00000', '00100'] +}; diff --git a/scientific-bounty-scope-change-guard/reports/demo.gif b/scientific-bounty-scope-change-guard/reports/demo.gif new file mode 100644 index 0000000000000000000000000000000000000000..d5aa865f0324828be196edb446b643634940ec31 GIT binary patch literal 16442 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

GLegi+Y&Skd3B3Q!VAqOw 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 + + +Scientific bounty scope-change guard +Overall decision: HOLD | Requests: 3 +Reviewer queue +scope-change-unsafe-rubric-and-prizeHOLDPause scoring for affected challengescope-change-safe-copy-clarificationALLOWRecord audit digestscope-change-reviewed-data-room-updateREVIEWPause scoring for affected challenge +Synthetic data only. Blocks goalpost-moving changes before scoring or payout. + \ No newline at end of file diff --git a/scientific-bounty-scope-change-guard/sample-data.js b/scientific-bounty-scope-change-guard/sample-data.js new file mode 100644 index 00000000..de0589ab --- /dev/null +++ b/scientific-bounty-scope-change-guard/sample-data.js @@ -0,0 +1,126 @@ +const baseRubric = [ + { id: 'accuracy', label: 'Scientific accuracy', weight: 35, maxPoints: 35 }, + { id: 'reproducibility', label: 'Reproducibility evidence', weight: 30, maxPoints: 30 }, + { id: 'novelty', label: 'Novelty and impact', weight: 20, maxPoints: 20 }, + { id: 'delivery', label: 'Deliverable completeness', weight: 15, maxPoints: 15 } +]; + +const challenge = { + challengeId: 'sci-bounty-biomarker-2026', + title: 'Single-cell biomarker discovery challenge', + launchedAt: '2026-05-01T12:00:00Z', + submissionDeadline: '2026-06-01T23:59:00Z', + baseline: { + problemStatement: 'Identify robust biomarkers from the released single-cell RNA-seq cohort.', + deliverables: ['working notebook', 'ranked biomarker CSV', 'methodology memo'], + rubric: baseRubric, + eligibility: { allowedTeams: ['academic', 'independent', 'industry'], requiresPriorContribution: false }, + prizeSchedule: { totalUsd: 100000, milestones: [{ name: 'winner', usd: 80000 }, { name: 'runner-up', usd: 20000 }] }, + ndaTerms: { version: 'nda-v1', dataUse: 'challenge-only' }, + dataRoomTerms: { version: 'data-room-v1', exportAllowed: false } + }, + participants: [ + { teamId: 'lab-alpha', status: 'submitted', registeredAt: '2026-05-02T10:00:00Z', submittedAt: '2026-05-21T15:00:00Z' }, + { teamId: 'team-beta', status: 'registered', registeredAt: '2026-05-04T11:30:00Z' }, + { teamId: 'solo-gamma', status: 'registered', registeredAt: '2026-05-09T09:00:00Z' } + ] +}; + +const unsafeScopeChange = { + id: 'scope-change-unsafe-rubric-and-prize', + requestedAt: '2026-05-22T09:00:00Z', + effectiveAt: '2026-05-23T09:00:00Z', + requestedBy: 'sponsor:pharma-demo', + changes: { + deliverables: ['working notebook', 'ranked biomarker CSV', 'methodology memo', 'wet-lab validation plan'], + rubric: [ + { id: 'accuracy', label: 'Scientific accuracy', weight: 30, maxPoints: 30 }, + { id: 'reproducibility', label: 'Reproducibility evidence', weight: 20, maxPoints: 20 }, + { id: 'wetlab', label: 'Wet-lab validation readiness', weight: 30, maxPoints: 30 }, + { id: 'delivery', label: 'Deliverable completeness', weight: 10, maxPoints: 10 } + ], + prizeSchedule: { totalUsd: 90000, milestones: [{ name: 'winner', usd: 90000 }] }, + eligibility: { allowedTeams: ['industry'], requiresPriorContribution: true } + }, + approvals: { + sponsor: true, + independentReviewer: false, + auditLog: false, + financeEscrow: false + }, + notice: { + sentAt: '2026-05-22T12:00:00Z', + teamNotices: ['lab-alpha'], + teamConsents: [] + }, + requiresSolverConsent: true, + grandfatherSubmittedWork: false, + grandfatherRegisteredTeams: false, + deadlineExtensionHours: 0 +}; + +const safeClarification = { + id: 'scope-change-safe-copy-clarification', + requestedAt: '2026-05-07T09:00:00Z', + effectiveAt: '2026-05-12T09:00:00Z', + requestedBy: 'ops:challenge-admin', + changes: { + faq: 'Clarifies that CSV headers may use snake_case or camelCase.' + }, + approvals: { + sponsor: true, + independentReviewer: true, + auditLog: true, + financeEscrow: true + }, + notice: { + sentAt: '2026-05-07T09:15:00Z', + teamNotices: ['lab-alpha', 'team-beta', 'solo-gamma'], + teamConsents: ['lab-alpha', 'team-beta', 'solo-gamma'] + }, + requiresSolverConsent: false, + grandfatherSubmittedWork: true, + grandfatherRegisteredTeams: true, + deadlineExtensionHours: 0, + equalDataRoomAccess: true +}; + +const reviewedNdaUpdate = { + id: 'scope-change-reviewed-data-room-update', + requestedAt: '2026-05-10T10:00:00Z', + effectiveAt: '2026-05-16T10:00:00Z', + requestedBy: 'sponsor:data-office', + changes: { + ndaTerms: { version: 'nda-v2', dataUse: 'challenge-only', addsPublicationEmbargoDays: 30 }, + dataRoomTerms: { version: 'data-room-v2', exportAllowed: false, watermarkRequired: true } + }, + approvals: { + sponsor: true, + independentReviewer: true, + auditLog: true, + financeEscrow: true + }, + notice: { + sentAt: '2026-05-10T10:10:00Z', + teamNotices: ['lab-alpha', 'team-beta', 'solo-gamma'], + teamConsents: ['lab-alpha', 'team-beta', 'solo-gamma'] + }, + requiresSolverConsent: true, + grandfatherSubmittedWork: true, + grandfatherRegisteredTeams: true, + deadlineExtensionHours: 120, + equalDataRoomAccess: true +}; + +module.exports = { + challenge, + unsafeScopeChange, + safeClarification, + reviewedNdaUpdate, + reviewInput: { + generatedAt: '2026-05-31T05:58:00Z', + challenge, + changeRequests: [unsafeScopeChange, safeClarification, reviewedNdaUpdate] + } +}; + diff --git a/scientific-bounty-scope-change-guard/test.js b/scientific-bounty-scope-change-guard/test.js new file mode 100644 index 00000000..629eda29 --- /dev/null +++ b/scientific-bounty-scope-change-guard/test.js @@ -0,0 +1,84 @@ +const assert = require('assert'); +const { + evaluateScopeChangeControl, + evaluateChangeRequest, + compareRubric, + digest +} = require('./index'); +const { + challenge, + unsafeScopeChange, + safeClarification, + reviewedNdaUpdate, + reviewInput +} = require('./sample-data'); + +function codes(result) { + return new Set(result.findings.map((finding) => finding.code)); +} + +function testUnsafeScopeChangeIsHeld() { + const result = evaluateChangeRequest(challenge, unsafeScopeChange); + const resultCodes = codes(result); + + assert.strictEqual(result.decision, 'hold'); + assert(resultCodes.has('MISSING_CHANGE_APPROVALS')); + assert(resultCodes.has('UNEQUAL_SOLVER_NOTICE')); + assert(resultCodes.has('SUBMITTED_WORK_NOT_GRANDFATHERED')); + assert(resultCodes.has('PRIZE_CHANGE_WITHOUT_ESCROW_EVIDENCE')); + assert(result.actions.includes('Grandfather submitted work under original scope')); +} + +function testSafeClarificationIsAllowed() { + const result = evaluateChangeRequest(challenge, safeClarification); + const resultCodes = codes(result); + + assert.strictEqual(result.decision, 'allow'); + assert(resultCodes.has('NON_MATERIAL_CHANGE')); + assert(!resultCodes.has('UNEQUAL_SOLVER_NOTICE')); +} + +function testReviewedNdaUpdateIsReviewOnly() { + const result = evaluateChangeRequest(challenge, reviewedNdaUpdate); + const resultCodes = codes(result); + + assert.strictEqual(result.decision, 'review'); + assert(resultCodes.has('MATERIAL_CHANGE_AFTER_LAUNCH')); + assert(!resultCodes.has('DATA_ROOM_ACCESS_NOT_RECONFIRMED')); + assert(!resultCodes.has('MISSING_SOLVER_CONSENT')); +} + +function testRubricComparison() { + const comparison = compareRubric(challenge.baseline.rubric, unsafeScopeChange.changes.rubric); + + assert.deepStrictEqual(comparison.added, ['wetlab']); + assert.deepStrictEqual(comparison.removed, ['novelty']); + assert.strictEqual(comparison.beforeTotal, 100); + assert.strictEqual(comparison.afterTotal, 90); +} + +function testOverallPacket() { + const evaluation = evaluateScopeChangeControl(reviewInput); + + assert.strictEqual(evaluation.overallDecision, 'hold'); + assert.strictEqual(evaluation.requestCount, 3); + assert.deepStrictEqual(evaluation.reviewerPacket.heldRequests, ['scope-change-unsafe-rubric-and-prize']); + assert.deepStrictEqual(evaluation.reviewerPacket.reviewRequests, ['scope-change-reviewed-data-room-update']); + assert.strictEqual(evaluation.reviewerPacket.baselineDigest, digest(challenge.baseline)); +} + +const tests = [ + testUnsafeScopeChangeIsHeld, + testSafeClarificationIsAllowed, + testReviewedNdaUpdateIsReviewOnly, + testRubricComparison, + testOverallPacket +]; + +for (const test of tests) { + test(); + console.log(`ok - ${test.name}`); +} + +console.log(`${tests.length} tests passed`); + From e4d297fa6aab77099cf6d6b873e310d4e4a05c1e Mon Sep 17 00:00:00 2001 From: attaboy11 Date: Sun, 31 May 2026 06:46:02 +0100 Subject: [PATCH 2/2] Add MP4 demo artifact for scope guard --- .../README.md | 1 + .../render-video.js | 104 +++++++++--------- .../reports/demo.mp4 | Bin 0 -> 16500 bytes 3 files changed, 56 insertions(+), 49 deletions(-) create mode 100644 scientific-bounty-scope-change-guard/reports/demo.mp4 diff --git a/scientific-bounty-scope-change-guard/README.md b/scientific-bounty-scope-change-guard/README.md index ed7583c8..63455800 100644 --- a/scientific-bounty-scope-change-guard/README.md +++ b/scientific-bounty-scope-change-guard/README.md @@ -20,6 +20,7 @@ The module reviews sponsor-requested changes after a scientific bounty is launch - `test.js` - dependency-free tests using Node's built-in `assert` - `demo.js` - generates reviewer JSON, Markdown, and SVG reports - `render-video.js` - renders a short MP4 with `ffmpeg`, or an animated GIF fallback with ImageMagick +- `reports/demo.mp4` / `reports/demo.gif` - reviewer demo artifact ## Validation diff --git a/scientific-bounty-scope-change-guard/render-video.js b/scientific-bounty-scope-change-guard/render-video.js index 5270b036..64acbf9f 100644 --- a/scientific-bounty-scope-change-guard/render-video.js +++ b/scientific-bounty-scope-change-guard/render-video.js @@ -4,47 +4,57 @@ const { spawnSync } = require('child_process'); const output = path.join(__dirname, 'reports', 'demo.mp4'); const gifOutput = path.join(__dirname, 'reports', 'demo.gif'); -const filter = [ - "drawtext=fontcolor=white:fontsize=36:x=48:y=60:text='SCIBASE scientific bounty scope guard'", - "drawtext=fontcolor=white:fontsize=28:x=48:y=140:text='HOLD unsafe post-launch changes'", - "drawtext=fontcolor=0xCBD5E1:fontsize=24:x=48:y=210:text='Checks rubric, deliverables, prize, eligibility, NDA, notices'", - "drawtext=fontcolor=0xCBD5E1:fontsize=24:x=48:y=260:text='Grandfathers submitted work and requires equal solver notice'", - "drawtext=fontcolor=0xFCA5A5:fontsize=30:x=48:y=350:text='Demo output: one hold, one review, one allow'" -].join(','); - -const ffmpegReady = spawnSync('ffmpeg', ['-version'], { stdio: 'ignore' }).status === 0; -const result = ffmpegReady - ? spawnSync( - 'ffmpeg', - [ - '-y', - '-f', - 'lavfi', - '-i', - 'color=c=0f172a:s=960x540:d=5', - '-vf', - filter, - '-pix_fmt', - 'yuv420p', - output - ], - { stdio: 'inherit' } - ) - : { status: 1 }; - -if (result.status === 0) { - console.log(`Rendered ${output}`); - process.exit(0); -} - -console.log('ffmpeg is unavailable; trying ImageMagick GIF fallback.'); -setImmediate(renderGifFallback); +const reportsDir = path.join(__dirname, 'reports'); -function renderGifFallback() { - const reportsDir = path.join(__dirname, 'reports'); +function main() { fs.mkdirSync(reportsDir, { recursive: true }); + const ffmpegReady = spawnSync('ffmpeg', ['-version'], { stdio: 'ignore' }).status === 0; + const frames = writeFrames(); + + try { + if (ffmpegReady) { + const result = spawnSync( + 'ffmpeg', + [ + '-y', + '-framerate', + '1', + '-i', + path.join(reportsDir, 'frame-%02d.ppm'), + '-vf', + 'scale=960:540:flags=neighbor,format=yuv420p', + '-movflags', + '+faststart', + '-r', + '24', + output + ], + { stdio: 'inherit' } + ); + + if (result.status === 0) { + console.log(`Rendered ${output}`); + return; + } + } + + console.log(ffmpegReady ? 'ffmpeg render failed; trying ImageMagick GIF fallback.' : 'ffmpeg is unavailable; trying ImageMagick GIF fallback.'); + const magick = spawnSync('magick', ['-delay', '140', '-loop', '0', ...frames, gifOutput], { + stdio: 'inherit' + }); + + if (magick.status !== 0) { + process.exit(magick.status || 1); + } + + console.log(`Rendered ${gifOutput}`); + } finally { + cleanupFrames(frames); + } +} - const frames = [ +function writeFrames() { + return [ { name: 'frame-01.ppm', title: 'SCOPE GUARD', @@ -74,20 +84,14 @@ function renderGifFallback() { fs.writeFileSync(file, ppmFrame(frame.title, frame.subtitle, frame.accent)); return file; }); +} - const magick = spawnSync('magick', ['-delay', '140', '-loop', '0', ...frames, gifOutput], { - stdio: 'inherit' - }); - - if (magick.status !== 0) { - process.exit(magick.status || 1); - } - +function cleanupFrames(frames) { for (const frame of frames) { - fs.unlinkSync(frame); + if (fs.existsSync(frame)) { + fs.unlinkSync(frame); + } } - - console.log(`Rendered ${gifOutput}`); } function ppmFrame(title, subtitle, accent) { @@ -171,3 +175,5 @@ const FONT = { 8: ['01110', '10001', '10001', '01110', '10001', '10001', '01110'], '?': ['11110', '00001', '00010', '00100', '00100', '00000', '00100'] }; + +main(); diff --git a/scientific-bounty-scope-change-guard/reports/demo.mp4 b/scientific-bounty-scope-change-guard/reports/demo.mp4 new file mode 100644 index 0000000000000000000000000000000000000000..f869c7f9ed83f75288dab92e3fde7d90bcee672b GIT binary patch literal 16500 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 cCaE}