From 42fcb55ac9494b6833028a914f8692fb95f5646b Mon Sep 17 00:00:00 2001 From: Gwani-28 Date: Mon, 1 Jun 2026 22:37:21 +0900 Subject: [PATCH] Add sample chain-of-custody assistant --- sample-chain-of-custody-assistant/README.md | 77 +++++ .../demo/decision-flow.png | Bin 0 -> 40173 bytes .../demo/decision-flow.svg | 30 ++ .../demo/sample-chain-of-custody-demo.mp4 | Bin 0 -> 29300 bytes .../demo/sample-report.json | 119 +++++++ .../demo/sample-report.md | 105 ++++++ .../fixtures/sample-custody-packet.json | 176 ++++++++++ .../src/chain-custody-assistant.mjs | 322 ++++++++++++++++++ .../test/run-tests.mjs | 56 +++ 9 files changed, 885 insertions(+) create mode 100644 sample-chain-of-custody-assistant/README.md create mode 100644 sample-chain-of-custody-assistant/demo/decision-flow.png create mode 100644 sample-chain-of-custody-assistant/demo/decision-flow.svg create mode 100644 sample-chain-of-custody-assistant/demo/sample-chain-of-custody-demo.mp4 create mode 100644 sample-chain-of-custody-assistant/demo/sample-report.json create mode 100644 sample-chain-of-custody-assistant/demo/sample-report.md create mode 100644 sample-chain-of-custody-assistant/fixtures/sample-custody-packet.json create mode 100644 sample-chain-of-custody-assistant/src/chain-custody-assistant.mjs create mode 100644 sample-chain-of-custody-assistant/test/run-tests.mjs diff --git a/sample-chain-of-custody-assistant/README.md b/sample-chain-of-custody-assistant/README.md new file mode 100644 index 00000000..533c4e7a --- /dev/null +++ b/sample-chain-of-custody-assistant/README.md @@ -0,0 +1,77 @@ +# Sample Chain-of-Custody Review Assistant + +This module adds a deterministic assistant for reviewing wet-lab or clinical sample +chain-of-custody packets before AI-generated research conclusions are released. + +It is scoped to synthetic/sample metadata only. It does not call external APIs, +does not require credentials, and does not process real participant records. + +## Why This Belongs In The AI Research Assistant Suite + +AI review and reproducibility tools can miss a basic scientific failure mode: +the sample used to support a claim may have incomplete custody evidence. This +assistant checks whether every sample has enough documented handoffs, consent +links, storage controls, assay-run alignment, and reviewer signoff before the +project relies on the result. + +The assistant emits a deterministic `release`, `revise`, or `hold` decision with +evidence-linked findings and remediation steps. + +## Checks + +- Missing consent or participant-link evidence. +- Custody events with missing actor or signature fields. +- Time gaps beyond the configured maximum transit window. +- Storage temperature excursions against the sample's storage spec. +- Blinding leaks in reviewer-visible metadata. +- Assay runs that use quarantined samples, mismatched instruments, or missing QC. +- Conclusions that rely on samples with unresolved high-risk custody findings. +- Required reviewer signoff before release. + +## Usage + +```bash +node sample-chain-of-custody-assistant/src/chain-custody-assistant.mjs \ + sample-chain-of-custody-assistant/fixtures/sample-custody-packet.json \ + --format markdown +``` + +JSON output: + +```bash +node sample-chain-of-custody-assistant/src/chain-custody-assistant.mjs \ + sample-chain-of-custody-assistant/fixtures/sample-custody-packet.json \ + --format json +``` + +## Validation + +```bash +node sample-chain-of-custody-assistant/test/run-tests.mjs +``` + +Expected result: + +```text +sample chain-of-custody assistant tests passed +``` + +## Demo Artifacts + +- `demo/sample-report.md` +- `demo/sample-report.json` +- `demo/decision-flow.svg` +- `demo/decision-flow.png` +- `demo/sample-chain-of-custody-demo.mp4` + +## Scope Boundaries + +This assistant intentionally avoids: + +- live LIMS, EHR, cloud storage, or instrument connections; +- real participant or patient data; +- credential handling; +- legal, medical, or regulatory advice; +- replacing required human review. + +It is a pre-release guardrail that highlights evidence gaps for a reviewer. diff --git a/sample-chain-of-custody-assistant/demo/decision-flow.png b/sample-chain-of-custody-assistant/demo/decision-flow.png new file mode 100644 index 0000000000000000000000000000000000000000..dee9a71df6171d7c9acaa70a9eaa94d55d805566 GIT binary patch literal 40173 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 literal 0 HcmV?d00001 diff --git a/sample-chain-of-custody-assistant/demo/decision-flow.svg b/sample-chain-of-custody-assistant/demo/decision-flow.svg new file mode 100644 index 00000000..8d89e09c --- /dev/null +++ b/sample-chain-of-custody-assistant/demo/decision-flow.svg @@ -0,0 +1,30 @@ + + Sample chain-of-custody assistant decision flow + Diagram showing input custody packets flowing through deterministic checks to release revise or hold decisions. + + + + Custody packet + samples, events, claims + + + Deterministic checks + consent links + handoffs and time gaps + temperature excursions + assay QC and quarantine + + + RELEASE + + REVISE + + HOLD + Output: JSON, Markdown, evidence-linked findings, remediation, research gap prompts + + + + + + + diff --git a/sample-chain-of-custody-assistant/demo/sample-chain-of-custody-demo.mp4 b/sample-chain-of-custody-assistant/demo/sample-chain-of-custody-demo.mp4 new file mode 100644 index 0000000000000000000000000000000000000000..5ad358456e70f0575ffba4d8cd022cb19c4fe612 GIT binary patch literal 29300 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 aNW%=r!)cM231*65WtsphQ!n`A^Zx`7=#Y2- literal 0 HcmV?d00001 diff --git a/sample-chain-of-custody-assistant/demo/sample-report.json b/sample-chain-of-custody-assistant/demo/sample-report.json new file mode 100644 index 00000000..af33f98e --- /dev/null +++ b/sample-chain-of-custody-assistant/demo/sample-report.json @@ -0,0 +1,119 @@ +{ + "projectId": "SCI-CHAIN-001", + "title": "Cytokine drift after cold-chain interruption", + "generatedAt": "2026-06-01T12:00:00Z", + "assistant": "sample-chain-of-custody-review", + "decision": "hold", + "severityCounts": { + "critical": 4, + "high": 6, + "medium": 4, + "low": 0 + }, + "findings": [ + { + "severity": "critical", + "sampleId": "S-1001,S-1002", + "check": "conclusion-release-gate", + "evidence": "Conclusion C-1 relies on sample(s) with critical custody findings.", + "remediation": "Hold this conclusion until critical sample custody issues are resolved." + }, + { + "severity": "critical", + "sampleId": "S-1002", + "check": "assay-qc", + "evidence": "assay-run@2026-05-22T18:20:00Z has qcStatus=fail.", + "remediation": "Do not release conclusions using this assay until QC failure is resolved or rerun." + }, + { + "severity": "critical", + "sampleId": "S-1002", + "check": "consent-link", + "evidence": "No consentId is attached to the sample metadata.", + "remediation": "Attach a verified consent record or remove this sample from AI-supported conclusions." + }, + { + "severity": "critical", + "sampleId": "S-1002", + "check": "open-quarantine", + "evidence": "Open quarantine: temperature excursion pending QA disposition.", + "remediation": "Resolve QA quarantine or remove this sample from conclusions." + }, + { + "severity": "high", + "sampleId": "S-1001", + "check": "blinding-leak", + "evidence": "Reviewer-visible fields include group/arm metadata while blindingGroup=exposed.", + "remediation": "Mask treatment-arm fields before AI review or document that the review is intentionally unblinded." + }, + { + "severity": "high", + "sampleId": "S-1002", + "check": "blinding-leak", + "evidence": "Reviewer-visible fields include group/arm metadata while blindingGroup=exposed.", + "remediation": "Mask treatment-arm fields before AI review or document that the review is intentionally unblinded." + }, + { + "severity": "high", + "sampleId": "S-1002", + "check": "reviewer-signoff", + "evidence": "No reviewerSignoff record is present.", + "remediation": "Obtain accountable QA/reviewer signoff before release." + }, + { + "severity": "high", + "sampleId": "S-1002", + "check": "temperature-excursion", + "evidence": "storage@2026-05-22T16:45:00Z recorded 11.7C outside 2C-8C.", + "remediation": "Open a QA disposition, document stability impact, and quarantine the sample until resolved." + }, + { + "severity": "high", + "sampleId": "S-1003", + "check": "conclusion-release-gate", + "evidence": "Conclusion C-2 relies on sample(s) with high-risk custody findings.", + "remediation": "Revise confidence language and require reviewer acceptance before release." + }, + { + "severity": "high", + "sampleId": "S-1003", + "check": "required-custody-event", + "evidence": "Missing required custody event: assay-run.", + "remediation": "Add a signed assay-run custody event or exclude this sample from release." + }, + { + "severity": "medium", + "sampleId": "S-1002", + "check": "custody-time-gap", + "evidence": "collect to accession gap is 7.8 hours.", + "remediation": "Add intermediate courier/storage handoff evidence or downgrade conclusions that rely on this sample." + }, + { + "severity": "medium", + "sampleId": "S-1002", + "check": "handoff-signature", + "evidence": "accession@2026-05-22T16:30:00Z is missing actor or signature evidence.", + "remediation": "Add a signed handoff record with accountable actor and timestamp." + }, + { + "severity": "medium", + "sampleId": "S-1002", + "check": "required-custody-event", + "evidence": "Missing required custody event: archive.", + "remediation": "Add a signed archive custody event or exclude this sample from release." + }, + { + "severity": "medium", + "sampleId": "S-1003", + "check": "custody-time-gap", + "evidence": "storage to archive gap is 6.5 hours.", + "remediation": "Add intermediate courier/storage handoff evidence or downgrade conclusions that rely on this sample." + } + ], + "researchGapPrompts": [ + "Which biomarkers remain stable after the observed cold-chain excursion window?", + "Would blinded AI review change the severity or interpretation of the reported effect?", + "What courier or intermediate storage metadata should be captured to close shipment evidence gaps?", + "Can the failed assay be rerun from archived aliquots, or should the conclusion be limited to unaffected samples?" + ] +} diff --git a/sample-chain-of-custody-assistant/demo/sample-report.md b/sample-chain-of-custody-assistant/demo/sample-report.md new file mode 100644 index 00000000..0869a202 --- /dev/null +++ b/sample-chain-of-custody-assistant/demo/sample-report.md @@ -0,0 +1,105 @@ +# Sample Chain-of-Custody Review: SCI-CHAIN-001 + +Title: Cytokine drift after cold-chain interruption +Decision: HOLD + +## Severity Counts + +- Critical: 4 +- High: 6 +- Medium: 4 +- Low: 0 + +## Findings + +### [CRITICAL] conclusion-release-gate + +- Sample/conclusion: S-1001,S-1002 +- Evidence: Conclusion C-1 relies on sample(s) with critical custody findings. +- Remediation: Hold this conclusion until critical sample custody issues are resolved. + +### [CRITICAL] assay-qc + +- Sample/conclusion: S-1002 +- Evidence: assay-run@2026-05-22T18:20:00Z has qcStatus=fail. +- Remediation: Do not release conclusions using this assay until QC failure is resolved or rerun. + +### [CRITICAL] consent-link + +- Sample/conclusion: S-1002 +- Evidence: No consentId is attached to the sample metadata. +- Remediation: Attach a verified consent record or remove this sample from AI-supported conclusions. + +### [CRITICAL] open-quarantine + +- Sample/conclusion: S-1002 +- Evidence: Open quarantine: temperature excursion pending QA disposition. +- Remediation: Resolve QA quarantine or remove this sample from conclusions. + +### [HIGH] blinding-leak + +- Sample/conclusion: S-1001 +- Evidence: Reviewer-visible fields include group/arm metadata while blindingGroup=exposed. +- Remediation: Mask treatment-arm fields before AI review or document that the review is intentionally unblinded. + +### [HIGH] blinding-leak + +- Sample/conclusion: S-1002 +- Evidence: Reviewer-visible fields include group/arm metadata while blindingGroup=exposed. +- Remediation: Mask treatment-arm fields before AI review or document that the review is intentionally unblinded. + +### [HIGH] reviewer-signoff + +- Sample/conclusion: S-1002 +- Evidence: No reviewerSignoff record is present. +- Remediation: Obtain accountable QA/reviewer signoff before release. + +### [HIGH] temperature-excursion + +- Sample/conclusion: S-1002 +- Evidence: storage@2026-05-22T16:45:00Z recorded 11.7C outside 2C-8C. +- Remediation: Open a QA disposition, document stability impact, and quarantine the sample until resolved. + +### [HIGH] conclusion-release-gate + +- Sample/conclusion: S-1003 +- Evidence: Conclusion C-2 relies on sample(s) with high-risk custody findings. +- Remediation: Revise confidence language and require reviewer acceptance before release. + +### [HIGH] required-custody-event + +- Sample/conclusion: S-1003 +- Evidence: Missing required custody event: assay-run. +- Remediation: Add a signed assay-run custody event or exclude this sample from release. + +### [MEDIUM] custody-time-gap + +- Sample/conclusion: S-1002 +- Evidence: collect to accession gap is 7.8 hours. +- Remediation: Add intermediate courier/storage handoff evidence or downgrade conclusions that rely on this sample. + +### [MEDIUM] handoff-signature + +- Sample/conclusion: S-1002 +- Evidence: accession@2026-05-22T16:30:00Z is missing actor or signature evidence. +- Remediation: Add a signed handoff record with accountable actor and timestamp. + +### [MEDIUM] required-custody-event + +- Sample/conclusion: S-1002 +- Evidence: Missing required custody event: archive. +- Remediation: Add a signed archive custody event or exclude this sample from release. + +### [MEDIUM] custody-time-gap + +- Sample/conclusion: S-1003 +- Evidence: storage to archive gap is 6.5 hours. +- Remediation: Add intermediate courier/storage handoff evidence or downgrade conclusions that rely on this sample. + +## Research Gap Prompts + +- Which biomarkers remain stable after the observed cold-chain excursion window? +- Would blinded AI review change the severity or interpretation of the reported effect? +- What courier or intermediate storage metadata should be captured to close shipment evidence gaps? +- Can the failed assay be rerun from archived aliquots, or should the conclusion be limited to unaffected samples? + diff --git a/sample-chain-of-custody-assistant/fixtures/sample-custody-packet.json b/sample-chain-of-custody-assistant/fixtures/sample-custody-packet.json new file mode 100644 index 00000000..625d32ba --- /dev/null +++ b/sample-chain-of-custody-assistant/fixtures/sample-custody-packet.json @@ -0,0 +1,176 @@ +{ + "projectId": "SCI-CHAIN-001", + "title": "Cytokine drift after cold-chain interruption", + "generatedAt": "2026-06-01T12:00:00Z", + "policy": { + "maxTransitGapHours": 4, + "requiredEvents": ["collect", "accession", "storage", "assay-run", "archive"], + "releaseRequiresReviewerSignoff": true + }, + "conclusions": [ + { + "id": "C-1", + "text": "IL-6 increased in the exposed cohort.", + "sampleIds": ["S-1001", "S-1002"] + }, + { + "id": "C-2", + "text": "Control samples remained stable across the shipment window.", + "sampleIds": ["S-1003"] + } + ], + "samples": [ + { + "id": "S-1001", + "participantId": "P-101", + "consentId": "CONS-778", + "visibleFields": ["sample_id", "collection_day", "blinded_arm"], + "blindingGroup": "exposed", + "storageSpec": { + "minCelsius": 2, + "maxCelsius": 8 + }, + "custodyEvents": [ + { + "type": "collect", + "timestamp": "2026-05-22T09:10:00Z", + "location": "Clinic A", + "actor": "phlebotomist-12", + "signature": "sig-collect-1001" + }, + { + "type": "accession", + "timestamp": "2026-05-22T10:20:00Z", + "location": "Lab intake", + "actor": "tech-07", + "signature": "sig-accession-1001" + }, + { + "type": "storage", + "timestamp": "2026-05-22T10:40:00Z", + "location": "Cold room 2", + "actor": "tech-07", + "signature": "sig-storage-1001", + "temperatureCelsius": 4.2 + }, + { + "type": "assay-run", + "timestamp": "2026-05-22T13:35:00Z", + "location": "ELISA bay", + "actor": "assay-operator-2", + "signature": "sig-assay-1001", + "instrumentId": "ELISA-04", + "qcStatus": "pass" + }, + { + "type": "archive", + "timestamp": "2026-05-22T16:00:00Z", + "location": "Freezer B", + "actor": "tech-14", + "signature": "sig-archive-1001", + "temperatureCelsius": -80 + } + ], + "reviewerSignoff": { + "reviewer": "qa-reviewer-1", + "timestamp": "2026-05-23T09:00:00Z" + } + }, + { + "id": "S-1002", + "participantId": "P-102", + "consentId": "", + "visibleFields": ["sample_id", "collection_day", "treatment_arm"], + "blindingGroup": "exposed", + "storageSpec": { + "minCelsius": 2, + "maxCelsius": 8 + }, + "custodyEvents": [ + { + "type": "collect", + "timestamp": "2026-05-22T08:45:00Z", + "location": "Clinic B", + "actor": "phlebotomist-09", + "signature": "sig-collect-1002" + }, + { + "type": "accession", + "timestamp": "2026-05-22T16:30:00Z", + "location": "Lab intake", + "actor": "tech-11", + "signature": "" + }, + { + "type": "storage", + "timestamp": "2026-05-22T16:45:00Z", + "location": "Cold room 2", + "actor": "tech-11", + "signature": "sig-storage-1002", + "temperatureCelsius": 11.7 + }, + { + "type": "assay-run", + "timestamp": "2026-05-22T18:20:00Z", + "location": "ELISA bay", + "actor": "assay-operator-2", + "signature": "sig-assay-1002", + "instrumentId": "ELISA-04", + "qcStatus": "fail" + } + ], + "quarantine": { + "status": "open", + "reason": "temperature excursion pending QA disposition" + }, + "reviewerSignoff": null + }, + { + "id": "S-1003", + "participantId": "P-103", + "consentId": "CONS-889", + "visibleFields": ["sample_id", "collection_day"], + "blindingGroup": "control", + "storageSpec": { + "minCelsius": -90, + "maxCelsius": -60 + }, + "custodyEvents": [ + { + "type": "collect", + "timestamp": "2026-05-22T09:30:00Z", + "location": "Clinic A", + "actor": "phlebotomist-12", + "signature": "sig-collect-1003" + }, + { + "type": "accession", + "timestamp": "2026-05-22T10:15:00Z", + "location": "Lab intake", + "actor": "tech-07", + "signature": "sig-accession-1003" + }, + { + "type": "storage", + "timestamp": "2026-05-22T10:30:00Z", + "location": "Freezer A", + "actor": "tech-07", + "signature": "sig-storage-1003", + "temperatureCelsius": -78 + }, + { + "type": "archive", + "timestamp": "2026-05-22T17:00:00Z", + "location": "Freezer B", + "actor": "tech-14", + "signature": "sig-archive-1003", + "temperatureCelsius": -75 + } + ], + "reviewerSignoff": { + "reviewer": "qa-reviewer-1", + "timestamp": "2026-05-23T09:05:00Z" + } + } + ] +} diff --git a/sample-chain-of-custody-assistant/src/chain-custody-assistant.mjs b/sample-chain-of-custody-assistant/src/chain-custody-assistant.mjs new file mode 100644 index 00000000..9616d08a --- /dev/null +++ b/sample-chain-of-custody-assistant/src/chain-custody-assistant.mjs @@ -0,0 +1,322 @@ +#!/usr/bin/env node +import fs from "node:fs"; +import path from "node:path"; + +const SEVERITY_SCORE = { + critical: 4, + high: 3, + medium: 2, + low: 1 +}; + +function hoursBetween(startIso, endIso) { + return (new Date(endIso).getTime() - new Date(startIso).getTime()) / 36e5; +} + +function finding(severity, sampleId, check, evidence, remediation) { + return { severity, sampleId, check, evidence, remediation }; +} + +function eventLabel(event) { + return `${event.type}@${event.timestamp || "missing-time"}`; +} + +function sortEvents(events) { + return [...(events || [])].sort((a, b) => { + return new Date(a.timestamp || 0).getTime() - new Date(b.timestamp || 0).getTime(); + }); +} + +function analyzeSample(sample, packet) { + const findings = []; + const policy = packet.policy || {}; + const requiredEvents = policy.requiredEvents || []; + const events = sortEvents(sample.custodyEvents); + const eventTypes = new Set(events.map((event) => event.type)); + + if (!sample.consentId) { + findings.push(finding( + "critical", + sample.id, + "consent-link", + "No consentId is attached to the sample metadata.", + "Attach a verified consent record or remove this sample from AI-supported conclusions." + )); + } + + for (const required of requiredEvents) { + if (!eventTypes.has(required)) { + findings.push(finding( + required === "assay-run" ? "high" : "medium", + sample.id, + "required-custody-event", + `Missing required custody event: ${required}.`, + `Add a signed ${required} custody event or exclude this sample from release.` + )); + } + } + + for (const event of events) { + if (!event.actor || !event.signature) { + findings.push(finding( + "medium", + sample.id, + "handoff-signature", + `${eventLabel(event)} is missing actor or signature evidence.`, + "Add a signed handoff record with accountable actor and timestamp." + )); + } + + if (typeof event.temperatureCelsius === "number" && sample.storageSpec) { + const { minCelsius, maxCelsius } = sample.storageSpec; + const isArchiveFreezer = event.type === "archive" && event.temperatureCelsius < minCelsius; + if (!isArchiveFreezer && (event.temperatureCelsius < minCelsius || event.temperatureCelsius > maxCelsius)) { + findings.push(finding( + "high", + sample.id, + "temperature-excursion", + `${eventLabel(event)} recorded ${event.temperatureCelsius}C outside ${minCelsius}C-${maxCelsius}C.`, + "Open a QA disposition, document stability impact, and quarantine the sample until resolved." + )); + } + } + } + + for (let index = 1; index < events.length; index += 1) { + const previous = events[index - 1]; + const current = events[index]; + const gap = hoursBetween(previous.timestamp, current.timestamp); + if (gap > (policy.maxTransitGapHours || 4)) { + findings.push(finding( + "medium", + sample.id, + "custody-time-gap", + `${previous.type} to ${current.type} gap is ${gap.toFixed(1)} hours.`, + "Add intermediate courier/storage handoff evidence or downgrade conclusions that rely on this sample." + )); + } + } + + if (sample.blindingGroup && (sample.visibleFields || []).some((field) => /arm|group|treatment/i.test(field))) { + findings.push(finding( + "high", + sample.id, + "blinding-leak", + `Reviewer-visible fields include group/arm metadata while blindingGroup=${sample.blindingGroup}.`, + "Mask treatment-arm fields before AI review or document that the review is intentionally unblinded." + )); + } + + const assayEvents = events.filter((event) => event.type === "assay-run"); + for (const assay of assayEvents) { + if (!assay.instrumentId) { + findings.push(finding( + "medium", + sample.id, + "instrument-link", + `${eventLabel(assay)} has no instrumentId.`, + "Link the assay event to the instrument run and calibration record." + )); + } + if (assay.qcStatus && assay.qcStatus !== "pass") { + findings.push(finding( + "critical", + sample.id, + "assay-qc", + `${eventLabel(assay)} has qcStatus=${assay.qcStatus}.`, + "Do not release conclusions using this assay until QC failure is resolved or rerun." + )); + } + } + + if (sample.quarantine?.status === "open") { + findings.push(finding( + "critical", + sample.id, + "open-quarantine", + `Open quarantine: ${sample.quarantine.reason || "reason not provided"}.`, + "Resolve QA quarantine or remove this sample from conclusions." + )); + } + + if (policy.releaseRequiresReviewerSignoff && !sample.reviewerSignoff) { + findings.push(finding( + "high", + sample.id, + "reviewer-signoff", + "No reviewerSignoff record is present.", + "Obtain accountable QA/reviewer signoff before release." + )); + } + + return findings; +} + +function conclusionFindings(packet, sampleRisk) { + const findings = []; + for (const conclusion of packet.conclusions || []) { + const linkedFindings = (conclusion.sampleIds || []).flatMap((sampleId) => sampleRisk.get(sampleId) || []); + const maxSeverity = linkedFindings.reduce((max, item) => { + return Math.max(max, SEVERITY_SCORE[item.severity] || 0); + }, 0); + + if (maxSeverity >= SEVERITY_SCORE.critical) { + findings.push(finding( + "critical", + conclusion.sampleIds.join(","), + "conclusion-release-gate", + `Conclusion ${conclusion.id} relies on sample(s) with critical custody findings.`, + "Hold this conclusion until critical sample custody issues are resolved." + )); + } else if (maxSeverity >= SEVERITY_SCORE.high) { + findings.push(finding( + "high", + conclusion.sampleIds.join(","), + "conclusion-release-gate", + `Conclusion ${conclusion.id} relies on sample(s) with high-risk custody findings.`, + "Revise confidence language and require reviewer acceptance before release." + )); + } + } + return findings; +} + +function summarizeDecision(findings) { + const counts = findings.reduce((acc, item) => { + acc[item.severity] = (acc[item.severity] || 0) + 1; + return acc; + }, {}); + + let decision = "release"; + if ((counts.critical || 0) > 0) { + decision = "hold"; + } else if ((counts.high || 0) > 0 || (counts.medium || 0) >= 3) { + decision = "revise"; + } + + return { + decision, + counts: { + critical: counts.critical || 0, + high: counts.high || 0, + medium: counts.medium || 0, + low: counts.low || 0 + } + }; +} + +function makeGapPrompts(packet, findings) { + const checks = new Set(findings.map((item) => item.check)); + const prompts = []; + if (checks.has("temperature-excursion")) { + prompts.push("Which biomarkers remain stable after the observed cold-chain excursion window?"); + } + if (checks.has("blinding-leak")) { + prompts.push("Would blinded AI review change the severity or interpretation of the reported effect?"); + } + if (checks.has("custody-time-gap")) { + prompts.push("What courier or intermediate storage metadata should be captured to close shipment evidence gaps?"); + } + if (checks.has("assay-qc")) { + prompts.push("Can the failed assay be rerun from archived aliquots, or should the conclusion be limited to unaffected samples?"); + } + if (prompts.length === 0) { + prompts.push(`What custody evidence would most improve reviewer confidence for ${packet.projectId}?`); + } + return prompts; +} + +export function analyzePacket(packet) { + const perSampleFindings = new Map(); + const findings = []; + + for (const sample of packet.samples || []) { + const sampleFindings = analyzeSample(sample, packet); + perSampleFindings.set(sample.id, sampleFindings); + findings.push(...sampleFindings); + } + + findings.push(...conclusionFindings(packet, perSampleFindings)); + const summary = summarizeDecision(findings); + + return { + projectId: packet.projectId, + title: packet.title, + generatedAt: packet.generatedAt, + assistant: "sample-chain-of-custody-review", + decision: summary.decision, + severityCounts: summary.counts, + findings: findings.sort((a, b) => { + return (SEVERITY_SCORE[b.severity] || 0) - (SEVERITY_SCORE[a.severity] || 0) + || a.sampleId.localeCompare(b.sampleId) + || a.check.localeCompare(b.check); + }), + researchGapPrompts: makeGapPrompts(packet, findings) + }; +} + +export function renderMarkdown(report) { + const lines = [ + `# Sample Chain-of-Custody Review: ${report.projectId}`, + "", + `Title: ${report.title}`, + `Decision: ${report.decision.toUpperCase()}`, + "", + "## Severity Counts", + "", + `- Critical: ${report.severityCounts.critical}`, + `- High: ${report.severityCounts.high}`, + `- Medium: ${report.severityCounts.medium}`, + `- Low: ${report.severityCounts.low}`, + "", + "## Findings", + "" + ]; + + for (const item of report.findings) { + lines.push(`### [${item.severity.toUpperCase()}] ${item.check}`); + lines.push(""); + lines.push(`- Sample/conclusion: ${item.sampleId}`); + lines.push(`- Evidence: ${item.evidence}`); + lines.push(`- Remediation: ${item.remediation}`); + lines.push(""); + } + + lines.push("## Research Gap Prompts", ""); + for (const prompt of report.researchGapPrompts) { + lines.push(`- ${prompt}`); + } + lines.push(""); + + return lines.join("\n"); +} + +function parseArgs(argv) { + const args = [...argv]; + const inputPath = args.find((arg) => !arg.startsWith("--")); + const formatIndex = args.indexOf("--format"); + const format = formatIndex >= 0 ? args[formatIndex + 1] : "markdown"; + return { inputPath, format }; +} + +function main() { + const { inputPath, format } = parseArgs(process.argv.slice(2)); + if (!inputPath) { + console.error("Usage: node chain-custody-assistant.mjs [--format markdown|json]"); + process.exit(2); + } + + const absolutePath = path.resolve(inputPath); + const packet = JSON.parse(fs.readFileSync(absolutePath, "utf8")); + const report = analyzePacket(packet); + + if (format === "json") { + console.log(JSON.stringify(report, null, 2)); + } else { + console.log(renderMarkdown(report)); + } +} + +if (import.meta.url === `file://${process.argv[1]}`) { + main(); +} diff --git a/sample-chain-of-custody-assistant/test/run-tests.mjs b/sample-chain-of-custody-assistant/test/run-tests.mjs new file mode 100644 index 00000000..ecf60526 --- /dev/null +++ b/sample-chain-of-custody-assistant/test/run-tests.mjs @@ -0,0 +1,56 @@ +#!/usr/bin/env node +import assert from "node:assert/strict"; +import fs from "node:fs"; +import path from "node:path"; +import { analyzePacket, renderMarkdown } from "../src/chain-custody-assistant.mjs"; + +const fixturePath = path.resolve("sample-chain-of-custody-assistant/fixtures/sample-custody-packet.json"); +const packet = JSON.parse(fs.readFileSync(fixturePath, "utf8")); +const report = analyzePacket(packet); + +assert.equal(report.assistant, "sample-chain-of-custody-review"); +assert.equal(report.decision, "hold"); +assert.equal(report.severityCounts.critical, 4); +assert.equal(report.severityCounts.high, 6); +assert.ok(report.findings.some((finding) => finding.check === "consent-link" && finding.sampleId === "S-1002")); +assert.ok(report.findings.some((finding) => finding.check === "assay-qc" && finding.sampleId === "S-1002")); +assert.ok(report.findings.some((finding) => finding.check === "required-custody-event" && finding.sampleId === "S-1003")); +assert.ok(report.findings.some((finding) => finding.check === "conclusion-release-gate" && finding.sampleId.includes("S-1002"))); +assert.ok(report.researchGapPrompts.some((prompt) => prompt.includes("cold-chain"))); + +const markdown = renderMarkdown(report); +assert.match(markdown, /Decision: HOLD/); +assert.match(markdown, /Sample Chain-of-Custody Review/); +assert.match(markdown, /Research Gap Prompts/); + +const cleanPacket = { + projectId: "SCI-CLEAN", + title: "Clean custody packet", + policy: { + maxTransitGapHours: 4, + requiredEvents: ["collect", "accession", "storage", "assay-run", "archive"], + releaseRequiresReviewerSignoff: true + }, + conclusions: [{ id: "C-clean", sampleIds: ["S-clean"] }], + samples: [{ + id: "S-clean", + participantId: "P-clean", + consentId: "CONS-clean", + visibleFields: ["sample_id", "collection_day"], + storageSpec: { minCelsius: 2, maxCelsius: 8 }, + custodyEvents: [ + { type: "collect", timestamp: "2026-01-01T09:00:00Z", actor: "a", signature: "s", location: "clinic" }, + { type: "accession", timestamp: "2026-01-01T10:00:00Z", actor: "b", signature: "s", location: "lab" }, + { type: "storage", timestamp: "2026-01-01T10:15:00Z", actor: "b", signature: "s", location: "fridge", temperatureCelsius: 4 }, + { type: "assay-run", timestamp: "2026-01-01T11:15:00Z", actor: "c", signature: "s", location: "bench", instrumentId: "inst-1", qcStatus: "pass" }, + { type: "archive", timestamp: "2026-01-01T12:00:00Z", actor: "d", signature: "s", location: "archive", temperatureCelsius: -80 } + ], + reviewerSignoff: { reviewer: "qa", timestamp: "2026-01-01T13:00:00Z" } + }] +}; + +const cleanReport = analyzePacket(cleanPacket); +assert.equal(cleanReport.decision, "release"); +assert.equal(cleanReport.findings.length, 0); + +console.log("sample chain-of-custody assistant tests passed");