From d229d7367a5b8a1ff3722dbfb53167dc790d7ffd Mon Sep 17 00:00:00 2001 From: davidrsdiaz Date: Thu, 28 May 2026 04:18:59 -0500 Subject: [PATCH] Add challenge closeout retention guard --- .../README.md | 34 +++ .../artifacts/closeout-report.json | 143 +++++++++ .../artifacts/closeout-report.md | 82 ++++++ .../artifacts/closeout-summary.svg | 11 + .../artifacts/demo.mp4 | Bin 0 -> 16962 bytes .../data/closeout-records.json | 150 ++++++++++ .../demo.js | 25 ++ .../guard.js | 272 ++++++++++++++++++ .../make-demo-video.js | 167 +++++++++++ .../test.js | 49 ++++ 10 files changed, 933 insertions(+) create mode 100644 scientific-bounty-system/challenge-closeout-retention-guard/README.md create mode 100644 scientific-bounty-system/challenge-closeout-retention-guard/artifacts/closeout-report.json create mode 100644 scientific-bounty-system/challenge-closeout-retention-guard/artifacts/closeout-report.md create mode 100644 scientific-bounty-system/challenge-closeout-retention-guard/artifacts/closeout-summary.svg create mode 100644 scientific-bounty-system/challenge-closeout-retention-guard/artifacts/demo.mp4 create mode 100644 scientific-bounty-system/challenge-closeout-retention-guard/data/closeout-records.json create mode 100644 scientific-bounty-system/challenge-closeout-retention-guard/demo.js create mode 100644 scientific-bounty-system/challenge-closeout-retention-guard/guard.js create mode 100644 scientific-bounty-system/challenge-closeout-retention-guard/make-demo-video.js create mode 100644 scientific-bounty-system/challenge-closeout-retention-guard/test.js diff --git a/scientific-bounty-system/challenge-closeout-retention-guard/README.md b/scientific-bounty-system/challenge-closeout-retention-guard/README.md new file mode 100644 index 00000000..62ae82f1 --- /dev/null +++ b/scientific-bounty-system/challenge-closeout-retention-guard/README.md @@ -0,0 +1,34 @@ +# Challenge Closeout Retention Guard + +This module adds a focused post-challenge closeout guard for issue #18, Scientific Bounty System. + +It validates what happens after a scientific bounty has been awarded, cancelled, or closed without award: + +- Sponsor data-room access must be revoked after the closeout deadline. +- Restricted sponsor datasets must be returned, destroyed, or retained under an explicit legal hold. +- Solver IP transfer must stay blocked until settlement is funded and recorded. +- Reviewer/arbitration records must remain retained for the appeal window. +- Private challenge outputs must be redacted before any public winner announcement. +- Cancelled or no-award challenges must preserve solver work and compensation evidence. + +The guard is dependency-free and uses synthetic records only. It emits deterministic JSON, Markdown, SVG, and MP4 artifacts for reviewer inspection. + +## Files + +- `guard.js` - closeout policy engine and report renderers. +- `data/closeout-records.json` - synthetic post-challenge closeout records. +- `demo.js` - generates JSON, Markdown, and SVG artifacts in `artifacts/`. +- `make-demo-video.js` - builds a short local MP4 demo with `ffmpeg`. +- `test.js` - dependency-free regression checks using Node's built-in `assert`. + +## Verification + +```sh +node scientific-bounty-system/challenge-closeout-retention-guard/test.js +node scientific-bounty-system/challenge-closeout-retention-guard/demo.js +node scientific-bounty-system/challenge-closeout-retention-guard/make-demo-video.js +``` + +## Scope Boundary + +This is not another intake, rubric-readiness, scoring, arbitration, payout eligibility, sponsor scorecard, IP-redaction preview, data-room access grant, cancellation/no-award, or solver-withdrawal module. It focuses on the final closeout boundary after solver work has ended: revocation, destruction/return evidence, appeal-record retention, settlement-gated IP transfer, and public disclosure readiness. diff --git a/scientific-bounty-system/challenge-closeout-retention-guard/artifacts/closeout-report.json b/scientific-bounty-system/challenge-closeout-retention-guard/artifacts/closeout-report.json new file mode 100644 index 00000000..197080a0 --- /dev/null +++ b/scientific-bounty-system/challenge-closeout-retention-guard/artifacts/closeout-report.json @@ -0,0 +1,143 @@ +{ + "generatedBy": "challenge-closeout-retention-guard", + "packetId": "challenge-closeout-demo-001", + "generatedAt": "2026-05-28T00:00:00Z", + "decision": "hold-closeout", + "riskScore": 32, + "severityCounts": { + "critical": 2, + "high": 6, + "medium": 2, + "low": 0 + }, + "challenges": [ + { + "challengeId": "climate-model-prize", + "challengeTitle": "Regional climate downscaling model prize", + "outcome": "awarded", + "daysSinceClose": 8 + }, + { + "challengeId": "oncology-biomarker-private", + "challengeTitle": "Private oncology biomarker discovery challenge", + "outcome": "awarded", + "daysSinceClose": 14 + }, + { + "challengeId": "quantum-noise-cancelled", + "challengeTitle": "Quantum noise mitigation prototype", + "outcome": "cancelled", + "daysSinceClose": 3 + } + ], + "findings": [ + { + "challengeId": "oncology-biomarker-private", + "challengeTitle": "Private oncology biomarker discovery challenge", + "code": "active-data-room-access-after-closeout", + "severity": "critical", + "domain": "access revocation", + "evidence": "solver-team-beta still has solver access to sponsor-rnaseq-training-cohort 14 days after closeout; policy requires revocation within 2 days.", + "action": "Revoke access, rotate shared credentials, and record the revocation event before closeout is accepted.", + "closeoutHold": true + }, + { + "challengeId": "oncology-biomarker-private", + "challengeTitle": "Private oncology biomarker discovery challenge", + "code": "restricted-data-disposition-missing", + "severity": "high", + "domain": "data retention", + "evidence": "sponsor-rnaseq-training-cohort is restricted and has no destruction, return, or legal-hold disposition.", + "action": "Collect a destruction certificate, return receipt, or legal-hold order for the restricted dataset.", + "closeoutHold": true + }, + { + "challengeId": "oncology-biomarker-private", + "challengeTitle": "Private oncology biomarker discovery challenge", + "code": "destruction-evidence-missing", + "severity": "high", + "domain": "data retention", + "evidence": "sponsor-clinical-labels is marked destroyed without a certificate or audit evidence identifier.", + "action": "Attach the destruction certificate hash or reviewer-verifiable evidence identifier.", + "closeoutHold": true + }, + { + "challengeId": "oncology-biomarker-private", + "challengeTitle": "Private oncology biomarker discovery challenge", + "code": "ip-transferred-before-funded-settlement", + "severity": "critical", + "domain": "IP transfer", + "evidence": "IP status is transferred while settlement status is pending.", + "action": "Reverse the transfer state or block sponsor use until funded settlement is recorded.", + "closeoutHold": true + }, + { + "challengeId": "oncology-biomarker-private", + "challengeTitle": "Private oncology biomarker discovery challenge", + "code": "appeal-record-retention-too-short", + "severity": "high", + "domain": "appeal records", + "evidence": "Appeal records are retained for 32 days, below the 60-day policy.", + "action": "Extend arbitration and appeal record retention before deleting review evidence.", + "closeoutHold": true + }, + { + "challengeId": "oncology-biomarker-private", + "challengeTitle": "Private oncology biomarker discovery challenge", + "code": "private-challenge-disclosure-unredacted", + "severity": "high", + "domain": "public disclosure", + "evidence": "Public disclosure is enabled for a private challenge without approved redactions.", + "action": "Block announcement until sponsor data, solver trade secrets, and reviewer identities are redacted.", + "closeoutHold": true + }, + { + "challengeId": "oncology-biomarker-private", + "challengeTitle": "Private oncology biomarker discovery challenge", + "code": "closeout-audit-digest-unsigned", + "severity": "medium", + "domain": "audit evidence", + "evidence": "Closeout audit digest status is draft.", + "action": "Sign the closeout digest after revocation, retention, and settlement checks complete.", + "closeoutHold": false + }, + { + "challengeId": "quantum-noise-cancelled", + "challengeTitle": "Quantum noise mitigation prototype", + "code": "cancelled-challenge-compensation-missing", + "severity": "high", + "domain": "solver protection", + "evidence": "The challenge was cancelled after work started, but no partial-compensation decision is recorded.", + "action": "Record partial compensation, refund, or no-compensation rationale before final closeout.", + "closeoutHold": true + }, + { + "challengeId": "quantum-noise-cancelled", + "challengeTitle": "Quantum noise mitigation prototype", + "code": "appeal-record-retention-too-short", + "severity": "high", + "domain": "appeal records", + "evidence": "Appeal records are retained for 11 days, below the 30-day policy.", + "action": "Extend arbitration and appeal record retention before deleting review evidence.", + "closeoutHold": true + }, + { + "challengeId": "quantum-noise-cancelled", + "challengeTitle": "Quantum noise mitigation prototype", + "code": "closeout-audit-digest-unsigned", + "severity": "medium", + "domain": "audit evidence", + "evidence": "Closeout audit digest status is unsigned.", + "action": "Sign the closeout digest after revocation, retention, and settlement checks complete.", + "closeoutHold": false + } + ], + "closeoutActions": [ + "Approve redactions before publishing private-challenge outcomes.", + "Collect destruction, return, or legal-hold evidence for restricted sponsor datasets.", + "Gate IP transfer and sponsor use until funded settlement is recorded.", + "Publish solver-facing closeout rationale and compensation decisions.", + "Retain arbitration records through the policy appeal window.", + "Revoke stale data-room and reviewer access, then rotate shared challenge credentials." + ] +} diff --git a/scientific-bounty-system/challenge-closeout-retention-guard/artifacts/closeout-report.md b/scientific-bounty-system/challenge-closeout-retention-guard/artifacts/closeout-report.md new file mode 100644 index 00000000..5c57aa1a --- /dev/null +++ b/scientific-bounty-system/challenge-closeout-retention-guard/artifacts/closeout-report.md @@ -0,0 +1,82 @@ +# Challenge Closeout Retention Guard + +Decision: hold-closeout +Risk score: 32 + +## Severity Counts + +| Severity | Count | +| --- | ---: | +| critical | 2 | +| high | 6 | +| medium | 2 | +| low | 0 | + +## Challenge Summary + +| Challenge | Outcome | Days Since Close | +| --- | --- | ---: | +| Regional climate downscaling model prize | awarded | 8 | +| Private oncology biomarker discovery challenge | awarded | 14 | +| Quantum noise mitigation prototype | cancelled | 3 | + +## Findings + +### CRITICAL: active-data-room-access-after-closeout +Challenge: Private oncology biomarker discovery challenge +Evidence: solver-team-beta still has solver access to sponsor-rnaseq-training-cohort 14 days after closeout; policy requires revocation within 2 days. +Action: Revoke access, rotate shared credentials, and record the revocation event before closeout is accepted. + +### HIGH: restricted-data-disposition-missing +Challenge: Private oncology biomarker discovery challenge +Evidence: sponsor-rnaseq-training-cohort is restricted and has no destruction, return, or legal-hold disposition. +Action: Collect a destruction certificate, return receipt, or legal-hold order for the restricted dataset. + +### HIGH: destruction-evidence-missing +Challenge: Private oncology biomarker discovery challenge +Evidence: sponsor-clinical-labels is marked destroyed without a certificate or audit evidence identifier. +Action: Attach the destruction certificate hash or reviewer-verifiable evidence identifier. + +### CRITICAL: ip-transferred-before-funded-settlement +Challenge: Private oncology biomarker discovery challenge +Evidence: IP status is transferred while settlement status is pending. +Action: Reverse the transfer state or block sponsor use until funded settlement is recorded. + +### HIGH: appeal-record-retention-too-short +Challenge: Private oncology biomarker discovery challenge +Evidence: Appeal records are retained for 32 days, below the 60-day policy. +Action: Extend arbitration and appeal record retention before deleting review evidence. + +### HIGH: private-challenge-disclosure-unredacted +Challenge: Private oncology biomarker discovery challenge +Evidence: Public disclosure is enabled for a private challenge without approved redactions. +Action: Block announcement until sponsor data, solver trade secrets, and reviewer identities are redacted. + +### MEDIUM: closeout-audit-digest-unsigned +Challenge: Private oncology biomarker discovery challenge +Evidence: Closeout audit digest status is draft. +Action: Sign the closeout digest after revocation, retention, and settlement checks complete. + +### HIGH: cancelled-challenge-compensation-missing +Challenge: Quantum noise mitigation prototype +Evidence: The challenge was cancelled after work started, but no partial-compensation decision is recorded. +Action: Record partial compensation, refund, or no-compensation rationale before final closeout. + +### HIGH: appeal-record-retention-too-short +Challenge: Quantum noise mitigation prototype +Evidence: Appeal records are retained for 11 days, below the 30-day policy. +Action: Extend arbitration and appeal record retention before deleting review evidence. + +### MEDIUM: closeout-audit-digest-unsigned +Challenge: Quantum noise mitigation prototype +Evidence: Closeout audit digest status is unsigned. +Action: Sign the closeout digest after revocation, retention, and settlement checks complete. + +## Closeout Actions + +- Approve redactions before publishing private-challenge outcomes. +- Collect destruction, return, or legal-hold evidence for restricted sponsor datasets. +- Gate IP transfer and sponsor use until funded settlement is recorded. +- Publish solver-facing closeout rationale and compensation decisions. +- Retain arbitration records through the policy appeal window. +- Revoke stale data-room and reviewer access, then rotate shared challenge credentials. diff --git a/scientific-bounty-system/challenge-closeout-retention-guard/artifacts/closeout-summary.svg b/scientific-bounty-system/challenge-closeout-retention-guard/artifacts/closeout-summary.svg new file mode 100644 index 00000000..7b292cdd --- /dev/null +++ b/scientific-bounty-system/challenge-closeout-retention-guard/artifacts/closeout-summary.svg @@ -0,0 +1,11 @@ + + + +Challenge Closeout Guard +Decision: hold-closeout +critical2 +high6 +medium2 +low0 +Risk score: 32 | Challenges: 3 | Findings: 10 + diff --git a/scientific-bounty-system/challenge-closeout-retention-guard/artifacts/demo.mp4 b/scientific-bounty-system/challenge-closeout-retention-guard/artifacts/demo.mp4 new file mode 100644 index 0000000000000000000000000000000000000000..2cb4aefda16849a18dc7597f28856c9f6c90ad6d GIT binary patch literal 16962 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 literal 0 HcmV?d00001 diff --git a/scientific-bounty-system/challenge-closeout-retention-guard/data/closeout-records.json b/scientific-bounty-system/challenge-closeout-retention-guard/data/closeout-records.json new file mode 100644 index 00000000..21a775a3 --- /dev/null +++ b/scientific-bounty-system/challenge-closeout-retention-guard/data/closeout-records.json @@ -0,0 +1,150 @@ +{ + "packetId": "challenge-closeout-demo-001", + "generatedAt": "2026-05-28T00:00:00Z", + "challenges": [ + { + "id": "climate-model-prize", + "title": "Regional climate downscaling model prize", + "closeout": { + "outcome": "awarded", + "closedAt": "2026-05-20T00:00:00Z" + }, + "policy": { + "revokeAccessWithinDays": 3, + "retainAppealRecordsDays": 45 + }, + "dataGrants": [ + { + "actor": "solver-team-alpha", + "role": "solver", + "datasetId": "sponsor-weather-stations-v4", + "classification": "restricted", + "active": false, + "closeoutDisposition": "destroyed", + "evidenceId": "cert-8841" + }, + { + "actor": "external-reviewer-2", + "role": "reviewer", + "datasetId": "public-baseline-archive", + "classification": "public", + "active": false, + "closeoutDisposition": "retained", + "evidenceId": "audit-9910" + } + ], + "settlement": { + "status": "funded" + }, + "ipTransfer": { + "status": "transferred" + }, + "solverProtection": { + "partialCompensationDecision": "not-needed", + "noAwardReasonPublished": true + }, + "records": { + "appealRecordsRetainUntil": "2026-07-10T00:00:00Z", + "auditDigestStatus": "signed" + }, + "publicDisclosure": { + "enabled": true, + "privateChallenge": false, + "redactionApproved": true + } + }, + { + "id": "oncology-biomarker-private", + "title": "Private oncology biomarker discovery challenge", + "closeout": { + "outcome": "awarded", + "closedAt": "2026-05-14T00:00:00Z" + }, + "policy": { + "revokeAccessWithinDays": 2, + "retainAppealRecordsDays": 60 + }, + "dataGrants": [ + { + "actor": "solver-team-beta", + "role": "solver", + "datasetId": "sponsor-rnaseq-training-cohort", + "classification": "restricted", + "active": true, + "closeoutDisposition": "unresolved", + "evidenceId": null + }, + { + "actor": "reviewer-panel-a", + "role": "reviewer", + "datasetId": "sponsor-clinical-labels", + "classification": "restricted", + "active": false, + "closeoutDisposition": "destroyed", + "evidenceId": null + } + ], + "settlement": { + "status": "pending" + }, + "ipTransfer": { + "status": "transferred" + }, + "solverProtection": { + "partialCompensationDecision": "not-needed", + "noAwardReasonPublished": true + }, + "records": { + "appealRecordsRetainUntil": "2026-06-15T00:00:00Z", + "auditDigestStatus": "draft" + }, + "publicDisclosure": { + "enabled": true, + "privateChallenge": true, + "redactionApproved": false + } + }, + { + "id": "quantum-noise-cancelled", + "title": "Quantum noise mitigation prototype", + "closeout": { + "outcome": "cancelled", + "closedAt": "2026-05-25T00:00:00Z" + }, + "policy": { + "revokeAccessWithinDays": 5, + "retainAppealRecordsDays": 30 + }, + "dataGrants": [ + { + "actor": "solver-lab-gamma", + "role": "solver", + "datasetId": "public-noise-fixtures", + "classification": "public", + "active": false, + "closeoutDisposition": "retained", + "evidenceId": "audit-2237" + } + ], + "settlement": { + "status": "refund-pending" + }, + "ipTransfer": { + "status": "blocked" + }, + "solverProtection": { + "partialCompensationDecision": null, + "noAwardReasonPublished": false + }, + "records": { + "appealRecordsRetainUntil": "2026-06-05T00:00:00Z", + "auditDigestStatus": "unsigned" + }, + "publicDisclosure": { + "enabled": false, + "privateChallenge": false, + "redactionApproved": false + } + } + ] +} diff --git a/scientific-bounty-system/challenge-closeout-retention-guard/demo.js b/scientific-bounty-system/challenge-closeout-retention-guard/demo.js new file mode 100644 index 00000000..3724bb75 --- /dev/null +++ b/scientific-bounty-system/challenge-closeout-retention-guard/demo.js @@ -0,0 +1,25 @@ +'use strict'; + +const fs = require('fs'); +const path = require('path'); +const { + analyzeCloseoutRecords, + renderMarkdown, + renderSvg +} = require('./guard'); + +const root = __dirname; +const packetPath = path.join(root, 'data', 'closeout-records.json'); +const artifactsDir = path.join(root, 'artifacts'); +const packet = JSON.parse(fs.readFileSync(packetPath, 'utf8')); +const report = analyzeCloseoutRecords(packet); + +fs.mkdirSync(artifactsDir, { recursive: true }); +fs.writeFileSync(path.join(artifactsDir, 'closeout-report.json'), `${JSON.stringify(report, null, 2)}\n`); +fs.writeFileSync(path.join(artifactsDir, 'closeout-report.md'), renderMarkdown(report)); +fs.writeFileSync(path.join(artifactsDir, 'closeout-summary.svg'), renderSvg(report)); + +console.log(`decision=${report.decision}`); +console.log(`riskScore=${report.riskScore}`); +console.log(`findings=${report.findings.length}`); +console.log(`artifacts=${artifactsDir}`); diff --git a/scientific-bounty-system/challenge-closeout-retention-guard/guard.js b/scientific-bounty-system/challenge-closeout-retention-guard/guard.js new file mode 100644 index 00000000..adab3a51 --- /dev/null +++ b/scientific-bounty-system/challenge-closeout-retention-guard/guard.js @@ -0,0 +1,272 @@ +'use strict'; + +const severityWeight = { + critical: 5, + high: 3, + medium: 2, + low: 1 +}; + +const severityOrder = ['critical', 'high', 'medium', 'low']; + +function daysBetween(start, end) { + const msPerDay = 24 * 60 * 60 * 1000; + return Math.round((new Date(end).getTime() - new Date(start).getTime()) / msPerDay); +} + +function addFinding(findings, challenge, code, severity, domain, evidence, action) { + findings.push({ + challengeId: challenge.id, + challengeTitle: challenge.title, + code, + severity, + domain, + evidence, + action, + closeoutHold: severity === 'critical' || severity === 'high' + }); +} + +function evaluateChallenge(challenge, now = '2026-05-28T00:00:00Z') { + const findings = []; + const daysSinceClose = daysBetween(challenge.closeout.closedAt, now); + const revokeDeadlineDays = challenge.policy.revokeAccessWithinDays; + const retainAppealDays = challenge.policy.retainAppealRecordsDays; + + for (const grant of challenge.dataGrants) { + if (grant.active && daysSinceClose > revokeDeadlineDays) { + addFinding( + findings, + challenge, + 'active-data-room-access-after-closeout', + 'critical', + 'access revocation', + `${grant.actor} still has ${grant.role} access to ${grant.datasetId} ${daysSinceClose} days after closeout; policy requires revocation within ${revokeDeadlineDays} days.`, + 'Revoke access, rotate shared credentials, and record the revocation event before closeout is accepted.' + ); + } + + if (grant.classification === 'restricted' && grant.closeoutDisposition === 'unresolved') { + addFinding( + findings, + challenge, + 'restricted-data-disposition-missing', + 'high', + 'data retention', + `${grant.datasetId} is restricted and has no destruction, return, or legal-hold disposition.`, + 'Collect a destruction certificate, return receipt, or legal-hold order for the restricted dataset.' + ); + } + + if (grant.closeoutDisposition === 'destroyed' && !grant.evidenceId) { + addFinding( + findings, + challenge, + 'destruction-evidence-missing', + 'high', + 'data retention', + `${grant.datasetId} is marked destroyed without a certificate or audit evidence identifier.`, + 'Attach the destruction certificate hash or reviewer-verifiable evidence identifier.' + ); + } + } + + if (challenge.settlement.status !== 'funded' && challenge.ipTransfer.status === 'transferred') { + addFinding( + findings, + challenge, + 'ip-transferred-before-funded-settlement', + 'critical', + 'IP transfer', + `IP status is transferred while settlement status is ${challenge.settlement.status}.`, + 'Reverse the transfer state or block sponsor use until funded settlement is recorded.' + ); + } + + if (challenge.closeout.outcome === 'cancelled' && !challenge.solverProtection.partialCompensationDecision) { + addFinding( + findings, + challenge, + 'cancelled-challenge-compensation-missing', + 'high', + 'solver protection', + 'The challenge was cancelled after work started, but no partial-compensation decision is recorded.', + 'Record partial compensation, refund, or no-compensation rationale before final closeout.' + ); + } + + if (challenge.closeout.outcome === 'no-award' && !challenge.solverProtection.noAwardReasonPublished) { + addFinding( + findings, + challenge, + 'no-award-reason-not-published', + 'medium', + 'solver protection', + 'No-award closeout does not include a solver-visible reason packet.', + 'Publish a solver-facing reason packet that preserves private sponsor data.' + ); + } + + const appealAge = daysBetween(challenge.closeout.closedAt, challenge.records.appealRecordsRetainUntil); + if (appealAge < retainAppealDays) { + addFinding( + findings, + challenge, + 'appeal-record-retention-too-short', + 'high', + 'appeal records', + `Appeal records are retained for ${appealAge} days, below the ${retainAppealDays}-day policy.`, + 'Extend arbitration and appeal record retention before deleting review evidence.' + ); + } + + if (challenge.publicDisclosure.enabled && challenge.publicDisclosure.privateChallenge && !challenge.publicDisclosure.redactionApproved) { + addFinding( + findings, + challenge, + 'private-challenge-disclosure-unredacted', + 'high', + 'public disclosure', + 'Public disclosure is enabled for a private challenge without approved redactions.', + 'Block announcement until sponsor data, solver trade secrets, and reviewer identities are redacted.' + ); + } + + if (challenge.records.auditDigestStatus !== 'signed') { + addFinding( + findings, + challenge, + 'closeout-audit-digest-unsigned', + 'medium', + 'audit evidence', + `Closeout audit digest status is ${challenge.records.auditDigestStatus}.`, + 'Sign the closeout digest after revocation, retention, and settlement checks complete.' + ); + } + + return { + challengeId: challenge.id, + challengeTitle: challenge.title, + outcome: challenge.closeout.outcome, + daysSinceClose, + findings + }; +} + +function summarizeFindings(findings) { + return severityOrder.reduce((summary, severity) => { + summary[severity] = findings.filter((finding) => finding.severity === severity).length; + return summary; + }, {}); +} + +function buildCloseoutActions(findings) { + const actions = new Set(); + + for (const finding of findings) { + if (finding.domain === 'access revocation') actions.add('Revoke stale data-room and reviewer access, then rotate shared challenge credentials.'); + if (finding.domain === 'data retention') actions.add('Collect destruction, return, or legal-hold evidence for restricted sponsor datasets.'); + if (finding.domain === 'IP transfer') actions.add('Gate IP transfer and sponsor use until funded settlement is recorded.'); + if (finding.domain === 'solver protection') actions.add('Publish solver-facing closeout rationale and compensation decisions.'); + if (finding.domain === 'appeal records') actions.add('Retain arbitration records through the policy appeal window.'); + if (finding.domain === 'public disclosure') actions.add('Approve redactions before publishing private-challenge outcomes.'); + } + + return [...actions].sort(); +} + +function analyzeCloseoutRecords(packet) { + const challenges = packet.challenges.map((challenge) => evaluateChallenge(challenge, packet.generatedAt)); + const findings = challenges.flatMap((challenge) => challenge.findings); + const riskScore = findings.reduce((score, finding) => score + severityWeight[finding.severity], 0); + const decision = findings.some((finding) => finding.closeoutHold) + ? 'hold-closeout' + : 'closeout-ready'; + + return { + generatedBy: 'challenge-closeout-retention-guard', + packetId: packet.packetId, + generatedAt: packet.generatedAt, + decision, + riskScore, + severityCounts: summarizeFindings(findings), + challenges: challenges.map(({ findings: _findings, ...challenge }) => challenge), + findings, + closeoutActions: buildCloseoutActions(findings) + }; +} + +function renderMarkdown(report) { + const lines = [ + '# Challenge Closeout Retention Guard', + '', + `Decision: ${report.decision}`, + `Risk score: ${report.riskScore}`, + '', + '## Severity Counts', + '', + '| Severity | Count |', + '| --- | ---: |', + ...severityOrder.map((severity) => `| ${severity} | ${report.severityCounts[severity]} |`), + '', + '## Challenge Summary', + '', + '| Challenge | Outcome | Days Since Close |', + '| --- | --- | ---: |', + ...report.challenges.map((challenge) => `| ${challenge.challengeTitle} | ${challenge.outcome} | ${challenge.daysSinceClose} |`), + '', + '## Findings', + '' + ]; + + for (const finding of report.findings) { + lines.push(`### ${finding.severity.toUpperCase()}: ${finding.code}`); + lines.push(`Challenge: ${finding.challengeTitle}`); + lines.push(`Evidence: ${finding.evidence}`); + lines.push(`Action: ${finding.action}`); + lines.push(''); + } + + lines.push('## Closeout Actions'); + lines.push(''); + for (const action of report.closeoutActions) { + lines.push(`- ${action}`); + } + + return `${lines.join('\n')}\n`; +} + +function renderSvg(report) { + const total = Math.max(1, report.findings.length); + const colors = { + critical: '#991b1b', + high: '#dc2626', + medium: '#f59e0b', + low: '#2563eb' + }; + const bars = severityOrder.map((severity, index) => { + const count = report.severityCounts[severity]; + const width = 30 + Math.round((count / total) * 300); + const y = 92 + index * 42; + return `${severity}${count}`; + }).join('\n'); + + return [ + '', + '', + '', + 'Challenge Closeout Guard', + `Decision: ${report.decision}`, + bars, + `Risk score: ${report.riskScore} | Challenges: ${report.challenges.length} | Findings: ${report.findings.length}`, + '', + '' + ].join('\n'); +} + +module.exports = { + analyzeCloseoutRecords, + evaluateChallenge, + renderMarkdown, + renderSvg +}; diff --git a/scientific-bounty-system/challenge-closeout-retention-guard/make-demo-video.js b/scientific-bounty-system/challenge-closeout-retention-guard/make-demo-video.js new file mode 100644 index 00000000..3dab7742 --- /dev/null +++ b/scientific-bounty-system/challenge-closeout-retention-guard/make-demo-video.js @@ -0,0 +1,167 @@ +'use strict'; + +const fs = require('fs'); +const os = require('os'); +const path = require('path'); +const { spawnSync } = require('child_process'); + +const width = 640; +const height = 360; +const fps = 12; +const frames = 60; +const colors = { + bg: [248, 250, 252], + card: [255, 255, 255], + border: [203, 213, 225], + ink: [15, 23, 42], + muted: [71, 85, 105], + critical: [153, 27, 27], + high: [220, 38, 38], + medium: [245, 158, 11], + low: [37, 99, 235] +}; + +const glyphs = { + ' ': ['00000', '00000', '00000', '00000', '00000', '00000', '00000'], + '-': ['00000', '00000', '00000', '11110', '00000', '00000', '00000'], + '0': ['01110', '10001', '10011', '10101', '11001', '10001', '01110'], + '1': ['00100', '01100', '00100', '00100', '00100', '00100', '01110'], + '2': ['01110', '10001', '00001', '00010', '00100', '01000', '11111'], + '3': ['11110', '00001', '00001', '01110', '00001', '00001', '11110'], + '4': ['00010', '00110', '01010', '10010', '11111', '00010', '00010'], + '5': ['11111', '10000', '10000', '11110', '00001', '00001', '11110'], + '6': ['01110', '10000', '10000', '11110', '10001', '10001', '01110'], + '7': ['11111', '00001', '00010', '00100', '01000', '01000', '01000'], + '8': ['01110', '10001', '10001', '01110', '10001', '10001', '01110'], + '9': ['01110', '10001', '10001', '01111', '00001', '00001', '01110'], + 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', '10011', '10001', '10001', '01111'], + H: ['10001', '10001', '10001', '11111', '10001', '10001', '10001'], + I: ['11111', '00100', '00100', '00100', '00100', '00100', '11111'], + J: ['00111', '00010', '00010', '00010', '00010', '10010', '01100'], + K: ['10001', '10010', '10100', '11000', '10100', '10010', '10001'], + 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'], + Q: ['01110', '10001', '10001', '10001', '10101', '10010', '01101'], + 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', '01010', '01010', '00100'], + W: ['10001', '10001', '10001', '10101', '10101', '11011', '10001'], + X: ['10001', '01010', '00100', '00100', '01010', '10001', '10001'], + Y: ['10001', '01010', '00100', '00100', '00100', '00100', '00100'], + Z: ['11111', '00001', '00010', '00100', '01000', '10000', '11111'] +}; + +function setPixel(buffer, x, y, color) { + if (x < 0 || y < 0 || x >= width || y >= height) return; + const index = (y * width + x) * 3; + buffer[index] = color[0]; + buffer[index + 1] = color[1]; + buffer[index + 2] = color[2]; +} + +function rect(buffer, x, y, w, h, color) { + for (let py = Math.max(0, y); py < Math.min(height, y + h); py++) { + for (let px = Math.max(0, x); px < Math.min(width, x + w); px++) { + setPixel(buffer, px, py, color); + } + } +} + +function fill(buffer, color) { + for (let i = 0; i < buffer.length; i += 3) { + buffer[i] = color[0]; + buffer[i + 1] = color[1]; + buffer[i + 2] = color[2]; + } +} + +function drawText(buffer, text, x, y, scale, color) { + let cursor = x; + for (const char of text.toUpperCase()) { + const pattern = glyphs[char] || glyphs[' ']; + for (let row = 0; row < pattern.length; row++) { + for (let col = 0; col < pattern[row].length; col++) { + if (pattern[row][col] === '1') { + rect(buffer, cursor + col * scale, y + row * scale, scale, scale, color); + } + } + } + cursor += 6 * scale; + } +} + +function writeFrame(filePath, buffer) { + fs.writeFileSync(filePath, Buffer.concat([Buffer.from(`P6\n${width} ${height}\n255\n`), Buffer.from(buffer)])); +} + +function renderFrame(index, outputDir) { + const buffer = Buffer.alloc(width * height * 3); + const progress = (index + 1) / frames; + fill(buffer, colors.bg); + rect(buffer, 34, 30, 572, 300, colors.card); + rect(buffer, 34, 30, 572, 3, colors.border); + rect(buffer, 34, 327, 572, 3, colors.border); + rect(buffer, 34, 30, 3, 300, colors.border); + rect(buffer, 603, 30, 3, 300, colors.border); + + drawText(buffer, 'CLOSEOUT GUARD', 58, 58, 4, colors.ink); + drawText(buffer, 'HOLD CLOSEOUT', 58, 104, 3, colors.muted); + drawText(buffer, 'RISK SCORE 32', 58, 140, 3, colors.ink); + + const scale = Math.min(1, progress * 1.3); + drawText(buffer, 'CRITICAL 2', 58, 188, 3, colors.ink); + rect(buffer, 236, 186, Math.round(230 * scale), 26, colors.critical); + drawText(buffer, 'HIGH 6', 58, 226, 3, colors.ink); + rect(buffer, 236, 224, Math.round(205 * Math.max(0, progress - 0.15) * 1.35), 26, colors.high); + drawText(buffer, 'MEDIUM 2', 58, 264, 3, colors.ink); + rect(buffer, 236, 262, Math.round(120 * Math.max(0, progress - 0.3) * 1.6), 26, colors.medium); + + rect(buffer, 430, 105, 95, 95, colors.border); + rect(buffer, 454, 129, 47, 47, index % 16 < 8 ? colors.high : colors.critical); + drawText(buffer, 'REVOKE', 414, 218, 2, colors.muted); + drawText(buffer, 'RETAIN', 414, 252, 2, colors.muted); + + writeFrame(path.join(outputDir, `frame${String(index).padStart(3, '0')}.ppm`), buffer); +} + +function main() { + const root = __dirname; + const artifactsDir = path.join(root, 'artifacts'); + const outputPath = path.join(artifactsDir, 'demo.mp4'); + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'closeout-demo-')); + fs.mkdirSync(artifactsDir, { recursive: true }); + + for (let index = 0; index < frames; index++) renderFrame(index, tempDir); + + const result = spawnSync('ffmpeg', [ + '-y', + '-framerate', + String(fps), + '-i', + path.join(tempDir, 'frame%03d.ppm'), + '-c:v', + 'libx264', + '-pix_fmt', + 'yuv420p', + '-movflags', + '+faststart', + outputPath + ], { stdio: 'inherit' }); + + fs.rmSync(tempDir, { recursive: true, force: true }); + if (result.status !== 0) throw new Error('ffmpeg failed to create demo.mp4'); + console.log(outputPath); +} + +main(); diff --git a/scientific-bounty-system/challenge-closeout-retention-guard/test.js b/scientific-bounty-system/challenge-closeout-retention-guard/test.js new file mode 100644 index 00000000..40354213 --- /dev/null +++ b/scientific-bounty-system/challenge-closeout-retention-guard/test.js @@ -0,0 +1,49 @@ +'use strict'; + +const assert = require('assert/strict'); +const fs = require('fs'); +const path = require('path'); +const { + analyzeCloseoutRecords, + evaluateChallenge, + renderMarkdown, + renderSvg +} = require('./guard'); + +const packetPath = path.join(__dirname, 'data', 'closeout-records.json'); +const packet = JSON.parse(fs.readFileSync(packetPath, 'utf8')); +const report = analyzeCloseoutRecords(packet); + +assert.equal(report.generatedBy, 'challenge-closeout-retention-guard'); +assert.equal(report.packetId, 'challenge-closeout-demo-001'); +assert.equal(report.decision, 'hold-closeout'); +assert.equal(report.challenges.length, 3); +assert.ok(report.riskScore >= 25); +assert.ok(report.severityCounts.critical >= 2); +assert.ok(report.severityCounts.high >= 4); + +const oncologyFindings = report.findings.filter((finding) => finding.challengeId === 'oncology-biomarker-private'); +const oncologyCodes = new Set(oncologyFindings.map((finding) => finding.code)); +assert.ok(oncologyCodes.has('active-data-room-access-after-closeout')); +assert.ok(oncologyCodes.has('restricted-data-disposition-missing')); +assert.ok(oncologyCodes.has('destruction-evidence-missing')); +assert.ok(oncologyCodes.has('ip-transferred-before-funded-settlement')); +assert.ok(oncologyCodes.has('private-challenge-disclosure-unredacted')); + +const cleanChallenge = packet.challenges.find((challenge) => challenge.id === 'climate-model-prize'); +assert.deepEqual(evaluateChallenge(cleanChallenge, packet.generatedAt).findings, []); + +assert.ok(report.closeoutActions.some((action) => action.includes('Revoke stale data-room'))); +assert.ok(report.closeoutActions.some((action) => action.includes('Gate IP transfer'))); + +const markdown = renderMarkdown(report); +assert.ok(markdown.includes('Challenge Closeout Retention Guard')); +assert.ok(markdown.includes('active-data-room-access-after-closeout')); +assert.ok(markdown.includes('| critical |')); + +const svg = renderSvg(report); +assert.ok(svg.startsWith('