Chapter 4
Designing consent, risk, genomic and safety records that an auditor can verify.
The first three chapters described bioethics as a set of obligations: disclose, confirm understanding, protect the vulnerable, weigh risk against benefit, handle genetic information with special care. None of those obligations can be audited unless each fulfilment leaves a durable, structured trace. This chapter is about that trace. It walks through the record types that WIA-BIO-018 defines in its specification (§8.3.1) and in the reference TypeScript definitions (api/typescript/src/types.ts), explains what each field is for, and points out where the three sources of truth in the standard disagree with one another. Where they disagree, the chapter recommends one canonical shape and shows how to convert the others into it. The SDK functions that consume these records, and the arithmetic inside them, are the subject of Chapter 5; here the focus is on the data itself.
The organising idea of WIA-BIO-018 is that every ethical decision should become a record that can be inspected later. A consent conversation produces an InformedConsent record; a protocol sent to the ethics committee produces an IRBSubmission; a safety problem produces an AdverseEvent; an audit produces an AuditResult. The specification lists an "Audit Trail: complete record of ethical decisions" among the six components every conformant system must include (§8.1), and the data model is what makes that audit trail possible. If a field is not in the record, an auditor cannot verify it, however carefully the underlying conversation was conducted.
Three design goals follow from this. First, completeness of evidence: a record must capture enough to reconstruct why a decision was taken, not only what it was. The consent record therefore stores the comprehension assessment and the capacity judgement alongside the signatures, because a signature on its own proves nothing about whether consent was informed. Second, separation of observation from evaluation: the facts gathered from people (which disclosures were made, what the participant scored) are kept apart from the scores the software computes (complianceScore, isValid). The computed fields are optional in the type for exactly this reason: they are filled in after validation and can be recomputed when the rules change. Third, linkability across the study lifecycle: consent, risk analysis, adverse events and audits all refer to the same study and, where relevant, the same participant, so that a reviewer can move from an adverse event to the consent the participant actually signed.
Reading types.ts for the first time can be confusing because it mixes three different kinds of structure. It helps to sort them before looking at individual fields.
| Family | Purpose | Types in types.ts | Persist? |
|---|---|---|---|
| Records | Durable evidence of something that happened or was decided | InformedConsent, IRBProtocol, IRBSubmission, RiskBenefitAnalysis, GeneticData, IncidentalFinding, GeneEditingEvaluation, AdverseEvent, ProtocolDeviation, AuditResult | Yes, append-only |
| Requests | Input parameters for an SDK call | ConsentValidation, EthicalRiskAssessment | Optional (store with the result for reproducibility) |
| Results | Output of an SDK evaluation | ConsentValidationResult, EthicalRiskResult, GeneEditingDecision, PrincipleAssessment, EnhancedProtection | Yes, next to the record they evaluated |
The distinction matters in practice. InformedConsent (a record) and ConsentValidation (a request) describe the same consent event but with different field names and different granularity; Section 4.2 shows how to derive the request from the record. Supporting types such as Risk, Benefit, SignatureInfo, ComprehensionAssessment, StudyParticipants and ChildAssent are building blocks nested inside records rather than records in their own right.
The diagram below shows how the main records relate. Solid arrows are relationships the types express directly; the dotted link is one the types leave implicit.
[IRBProtocol] --1:n--> [IRBSubmission]
protocolId submissionId, reviewLevel, status,
riskLevel decision, expirationDate
participants{...}
:
: study link (no studyId field on IRBProtocol -- see 4.1)
v
( studyId ) ----+----------------+-----------------+-----------------+
| | | |
[InformedConsent] [RiskBenefitAnalysis] [AdverseEvent] [ProtocolDeviation]
consentId analysisId eventId deviationId
participantId risks: Risk[] participantId
elements{10} benefits: Benefit[] severity [AuditResult]
signatures{4} justiceFactor causality auditId
| findings[]
| participantId (pseudonymous code)
+--------+-----------------+
| |
[GeneticData] [IncidentalFinding]
dataId findingId
protectionLevel significance (5 tiers)
returnOfResults actionable, shouldReturn
[GeneEditingEvaluation] --evaluated into--> [GeneEditingDecision]
evaluationId, editingType, decision, conditions[],
purpose, risks{4} oversight[], monitoring[]
Two gaps are visible in the map and are worth fixing in any implementation. IRBProtocol has a protocolId but no studyId, while consent records, adverse events, deviations and audits are all keyed by studyId. Nothing in the types says how a protocol and a study are connected. The simplest remedy is to adopt the convention that protocolId equals studyId, or to add an explicit studyId field to the protocol record. Similarly, GeneticData, IncidentalFinding and GeneEditingEvaluation carry no studyId at all, and IncidentalFinding does not reference the dataId of the dataset in which it was found. A biobank serving several studies needs those links to answer basic questions such as "under which consent was this sample sequenced?", so this book recommends adding them as extension fields. Adding optional fields keeps records readable by any software that follows the published types.
Note on the reference file. types.ts ends with an export type { … } block and an export { … } block that re-export names already declared with export. A TypeScript compiler rejects this (errors TS2484 and TS2323 with TypeScript 5.9). Delete the two trailing blocks before using the file; the declarations themselves are unaffected.
InformedConsent is the central record of the standard. It captures one consent event: one participant agreeing, at one moment, to one version of one study's consent form. If the form changes and the participant re-consents, that is a new record, not an edit of the old one. The table lists every field.
| Field | Type | Required | Meaning and implementation notes |
|---|---|---|---|
consentId | string | yes | Unique identifier of this consent event. The specification example uses CONSENT-2025-001. |
participantId | string | yes | Pseudonymous study code (for example P-12345). Never a hospital record number or national ID. |
studyId | string | yes | The study the consent applies to. |
version | string | yes | Version of the consent form the participant saw (see 4.7). Not the version of the record schema. |
language | string | yes | Language of the form actually used. A BCP 47 tag such as en, ko or es-MX is the natural choice. |
consentDate | Date | yes | When consent was given; serialised as an ISO 8601 UTC timestamp. |
elements | ConsentFormElements | yes | Ten booleans, one per required disclosure (§3.1.1). See 4.3. |
participantCapacity | 'full' | 'borderline' | 'diminished' | 'severely-impaired' | yes | Outcome of the decision-making capacity assessment. The two lower bands require a legally authorized representative (LAR). |
comprehension | ComprehensionAssessment | no (in the type) | Method, score, raw counts, assessor, date and pass flag. Optional in the type, but this book treats it as mandatory because the SDK assumes a perfect score when it is missing (Chapter 5). |
voluntarinessScore | number 0–1 | yes | Result of screening for coercion indicators. The specification sets V ≥ 0.95 as its threshold (§3.1.3); the reference validateInformedConsent does not read this field, so it must be checked separately. |
signatures | object of SignatureInfo | yes | participant and investigator are required; legallyAuthorizedRepresentative and witness are conditional. |
witnessPresent | boolean | yes | Whether an impartial witness attended. Must agree with the presence of signatures.witness. |
isValid | boolean | no | Computed by validation; empty until validated. |
complianceScore | number 0–1 | no | Computed by validation; the weighted score described in Chapter 5. |
ComprehensionAssessment records method (teach-back, quiz, questionnaire or interview), score, correctResponses, totalQuestions, assessor, date and passed. Three of these fields are redundant by design: score should equal correctResponses / totalQuestions, and passed should equal score ≥ 0.8, the standard's comprehension threshold (§3.1.2). Keeping the raw counts is worthwhile because a reviewer can see that 0.8 came from 8 of 10 rather than 4 of 5, which says something about how searching the assessment was. The redundancy should be enforced, however: a record whose passed flag contradicts its score is corrupt, and a validator should reject it rather than trust either value.
SignatureInfo holds signed, an optional name, a date, an optional method (handwritten, digital, electronic or verbal) and an optional verified flag. The type does not say who verified a signature or how, and verified is a single boolean. For electronic signatures that must satisfy a regulation such as US 21 CFR Part 11, an implementation will need more than this — at least the identity-verification method and a link to the signed document version — and should store it in the audit trail described in Chapter 7. Note also that the participant signature has an optional name: that is deliberate, since the consent record is keyed by a pseudonymous participantId and the participant's legal name is normally kept in a separate, access-restricted identity table.
The specification gives one complete consent record in §8.3.1. It is reproduced below exactly as published.
{
"consentId": "CONSENT-2025-001",
"participantId": "P-12345",
"studyId": "STUDY-2025-CANCER",
"consentDate": "2025-12-26T10:30:00Z",
"version": "3.1",
"language": "en",
"elements": {
"purpose": true,
"procedures": true,
"duration": true,
"risks": true,
"benefits": true,
"alternatives": true,
"confidentiality": true,
"compensation": true,
"voluntary": true,
"withdrawal": true,
"contact": true
},
"comprehension": {
"score": 0.92,
"method": "teach-back",
"assessor": "RN-001"
},
"signatures": {
"participant": {
"signed": true,
"date": "2025-12-26T10:30:00Z",
"method": "digital"
},
"witness": {
"signed": true,
"name": "Jane Smith",
"date": "2025-12-26T10:30:00Z"
},
"investigator": {
"signed": true,
"name": "Dr. John Doe",
"date": "2025-12-26T10:35:00Z"
}
},
"validity": {
"isValid": true,
"complianceScore": 0.98
}
}
Read against the InformedConsent type, the example is recognisably the same record, but it differs in six places:
elements uses eleven short keys (purpose, procedures, … contact) instead of the ten descriptive booleans of ConsentFormElements, and has no key for future use of data and samples. Section 4.3 deals with this in detail.participantCapacity is missing. The SDK defaults an absent capacity to full, so a record that simply omits the field is treated as coming from a fully capable adult.voluntarinessScore and witnessPresent are missing. The witness signature is present, so a reader can infer that a witness attended, but the record does not say so explicitly.comprehension is partial: it has score, method and assessor but no raw counts, date or pass flag.method, and the participant's timestamp equals consentDate while the investigator signed five minutes later. That ordering (participant first, then the person obtaining consent) is normal practice and a useful consistency check.validity object, whereas the type has isValid and complianceScore at the top level.
It is instructive to check the example's complianceScore of 0.98 against the SDK's weighting (0.30 elements + 0.30 comprehension + 0.20 capacity + 0.15 signature + 0.05 witness). With all eleven elements true, comprehension 0.92, capacity defaulted to full (1.0), a participant signature and a witness signature, the SDK yields 0.30 + 0.276 + 0.20 + 0.15 + 0.05 = 0.976, which rounds to the published 0.98. The example is therefore internally consistent with the reference code. But if the same record is first converted to the ten canonical elements, the missing future-use disclosure becomes false: completeness drops to 0.9, the score to 0.946, and the SDK reports an incomplete-consent violation, so isValid becomes false. Whether the example is "valid" thus depends on which consent shape the validator is given. That is the strongest argument for fixing a single canonical shape.
The following record expresses the same consent event in the canonical form this book recommends: every field of InformedConsent present, the ten canonical elements, a complete comprehension assessment, and the computed fields at the top level. The raw counts (23 of 25) are illustrative values chosen to match the published score of 0.92.
{
"consentId": "CONSENT-2025-001",
"participantId": "P-12345",
"studyId": "STUDY-2025-CANCER",
"version": "3.1",
"language": "en",
"consentDate": "2025-12-26T10:30:00Z",
"elements": {
"purposeDisclosed": true,
"durationExplained": true,
"risksDisclosed": true,
"benefitsExplained": true,
"alternativesProvided": true,
"withdrawalRights": true,
"privacyProtection": true,
"compensationExplained": true,
"contactProvided": true,
"futureUseDisclosed": true
},
"participantCapacity": "full",
"comprehension": {
"method": "teach-back",
"score": 0.92,
"correctResponses": 23,
"totalQuestions": 25,
"assessor": "RN-001",
"date": "2025-12-26T10:24:00Z",
"passed": true
},
"voluntarinessScore": 1.0,
"witnessPresent": true,
"signatures": {
"participant": { "signed": true, "date": "2025-12-26T10:30:00Z", "method": "digital", "verified": true },
"witness": { "signed": true, "name": "Jane Smith", "date": "2025-12-26T10:30:00Z", "method": "digital" },
"investigator": { "signed": true, "name": "Dr. John Doe", "date": "2025-12-26T10:35:00Z", "method": "digital" }
},
"isValid": true,
"complianceScore": 0.976
}
Several consistency rules can be checked mechanically on a record like this before any scoring takes place: comprehension.score equals correctResponses / totalQuestions (23 / 25 = 0.92); passed agrees with the 0.8 threshold; comprehension.date precedes consentDate; witnessPresent is true if and only if a witness signature exists; the investigator's signature is not earlier than the participant's; and if participantCapacity is diminished or severely-impaired, a signed legallyAuthorizedRepresentative entry exists. The stored complianceScore of 0.976 is simply what the SDK computes for this input; storing it unrounded avoids the ambiguity of the published 0.98.
The standard describes the content of the consent form three times, in three different shapes. The TypeScript type ConsentFormElements has ten booleans. The §8.3.1 JSON example has eleven keys. The §8.2.1 API sketch of ConsentValidation.consentForm has six. They were evidently written at different times, and they do not line up one-to-one. The table aligns them against the ten required disclosures of §3.1.1.
| §3.1.1 required disclosure | ConsentFormElements (types.ts, 10) | §8.3.1 JSON elements (11) | §8.2.1 consentForm (6) |
|---|---|---|---|
| 1. Research purpose and procedures | purposeDisclosed | purpose + procedures | — |
| 2. Duration of participation | durationExplained | duration | — |
| 3. Risks | risksDisclosed | risks | risksDisclosed |
| 4. Benefits | benefitsExplained | benefits | benefitsExplained |
| 5. Alternatives to participation | alternativesProvided | alternatives | alternativesProvided |
| 6. Confidentiality measures | privacyProtection | confidentiality | privacyProtection |
| 7. Compensation for injury | compensationExplained | compensation | — |
| 8. Voluntary participation and right to withdraw | withdrawalRights | voluntary + withdrawal | voluntaryParticipation + withdrawalRights |
| 9. Contact information | contactProvided | contact | — |
| 10. Future use of data/samples | futureUseDisclosed | — (absent) | — |
Only ConsentFormElements covers all ten disclosures, and it is the only one that is also the actual parameter type of the SDK's validateInformedConsent. This book therefore treats the ten-field type as canonical. The other two shapes should be accepted, if at all, only at the edge of a system and converted immediately.
Why does this matter so much? Because the reference validator measures completeness as "the fraction of keys whose value is true", without checking which keys are present. A six-key form with all six keys true scores a completeness of 1.0 even though four of the ten required disclosures — duration, compensation, contact and future use — were never recorded. The eleven-key form scores 1.0 despite omitting future use, which is precisely the disclosure a biobank most needs. Conversely, splitting one disclosure into two keys (purpose and procedures) changes the weight each carries. The validator is only as good as the shape it is fed, and the shape is not enforced anywhere in the reference code.
Two conversion rules deserve attention. Where the eleven-key form splits one disclosure into two keys, the canonical value is the logical AND of both: the purpose was not adequately disclosed if the procedures were not. And an absent key must convert to false, never to true: silence in a consent record means the disclosure cannot be shown to have happened. A related ambiguity concerns item 7. The specification says "compensation for injury", while the type's comment says "compensation policy explained". These are different things — payment for time and travel versus what happens if the research causes harm — and a consent form should address both; the boolean should be set only when both were explained, or the model should be extended with a second flag.
import type { ConsentFormElements } from './types';
/** Shape of `elements` in the spec's §8.3.1 example (11 keys). */
interface SpecConsentElements {
purpose: boolean; procedures: boolean; duration: boolean;
risks: boolean; benefits: boolean; alternatives: boolean;
confidentiality: boolean; compensation: boolean;
voluntary: boolean; withdrawal: boolean; contact: boolean;
futureUse?: boolean; // absent from the spec example
}
const CANONICAL_KEYS: (keyof ConsentFormElements)[] = [
'purposeDisclosed', 'durationExplained', 'risksDisclosed', 'benefitsExplained',
'alternativesProvided', 'withdrawalRights', 'privacyProtection',
'compensationExplained', 'contactProvided', 'futureUseDisclosed',
];
export function fromSpecElements(e: SpecConsentElements): ConsentFormElements {
return {
purposeDisclosed: e.purpose && e.procedures, // §3.1.1 item 1
durationExplained: e.duration,
risksDisclosed: e.risks,
benefitsExplained: e.benefits,
alternativesProvided: e.alternatives,
withdrawalRights: e.voluntary && e.withdrawal, // §3.1.1 item 8
privacyProtection: e.confidentiality,
compensationExplained: e.compensation,
contactProvided: e.contact,
futureUseDisclosed: e.futureUse === true, // missing means NOT disclosed
};
}
export function assertCanonicalElements(x: Record<string, unknown>): ConsentFormElements {
const keys = Object.keys(x);
const unknown = keys.filter(k => !(CANONICAL_KEYS as string[]).includes(k));
const missing = CANONICAL_KEYS.filter(k => !(k in x));
if (unknown.length || missing.length) {
throw new Error(`E001: non-canonical elements (missing: ${missing.join(',')}; unknown: ${unknown.join(',')})`);
}
for (const k of CANONICAL_KEYS) {
if (typeof x[k] !== 'boolean') throw new Error(`E001: ${k} must be boolean`);
}
return x as unknown as ConsentFormElements;
}
Applied to the §8.3.1 example, fromSpecElements yields nine true values and futureUseDisclosed: false, reproducing the E001 outcome described above. assertCanonicalElements rejects both unknown keys and missing keys, so the six-key and eleven-key shapes can no longer reach the validator by accident.
Pitfall: three consent shapes, one validator. The reference validateInformedConsent accepts any object of booleans as consentForm and scores the fraction that are true. Feeding it the §8.2.1 six-key sketch or the §8.3.1 eleven-key example produces a perfect completeness score while required disclosures are missing. Normalise every inbound form to the ten canonical keys, convert absent keys to false, and reject unknown keys before scoring.
Risk-benefit review is the practical form of beneficence, and the standard gives it a structured record. Each risk is a Risk object and each benefit a Benefit object; a RiskBenefitAnalysis collects them for one study.
| Type | Fields | Ranges and meaning |
|---|---|---|
Risk | description, category, probability, severity, score, mitigation? | probability 0–1; severity 0–10; score = probability × severity (0–10) |
RiskCategory | physical, psychological, social, economic, privacy, legal | Kind of harm. Privacy and legal harms are separate from social and economic ones. |
Benefit | description, category, magnitude, probability, score | magnitude 0–10; probability 0–1; score = probability × magnitude |
BenefitCategory | direct-participant, knowledge, societal, scientific | Who benefits: the participant, or others through knowledge |
RiskLevel | minimal, minor-increase, greater-than-minimal, high | Overall study risk classification used for review routing (Chapter 6) |
RiskBenefitAnalysis | analysisId, studyId, risks[], totalRiskScore, benefits[], totalBenefitScore, riskBenefitRatio, justiceFactor, isEthicallyAcceptable, recommendation, rationale | justiceFactor 0–1; recommendation approve / approve-with-modifications / defer / reject |
Three properties of these records are easy to miss. First, score is a stored field, not a computed one. The reference SDK sums the scores it is given and never recomputes probability × severity, so a risk entered as probability 0.5, severity 8, score 0.4 (instead of 4.0) silently understates the risk tenfold. A validator should recompute every score and reject mismatches.
Second, the field named riskBenefitRatio actually holds benefit divided by risk, further multiplied by the justice factor. A value above 1 is favourable. Anyone reading the name literally would invert the interpretation. The denominator is also floored at 0.1 in the SDK, so a study with no listed risks does not divide by zero but gets a very large ratio — another reason why an empty risk list should itself be a review finding. Because only the adjusted ratio is stored, the unadjusted benefit/risk figure is lost; this book recommends recording both.
Third, RiskLevel has four values in types.ts but only three (without high) in the §8.2.2 API sketch. Data written by a system that emits high will be rejected by one that follows the sketch. Treat the four-value type as canonical.
The following analysis shows the record filled in. The numbers are illustrative, not drawn from any real study.
{
"analysisId": "RBA-1766745000000",
"studyId": "STUDY-2025-CANCER",
"risks": [
{ "description": "Infusion reaction", "category": "physical",
"probability": 0.10, "severity": 6, "score": 0.6 },
{ "description": "Re-identification of coded data", "category": "privacy",
"probability": 0.05, "severity": 4, "score": 0.2 },
{ "description": "Anxiety from frequent imaging", "category": "psychological",
"probability": 0.20, "severity": 2, "score": 0.4 }
],
"totalRiskScore": 1.2,
"benefits": [
{ "description": "Possible tumour response", "category": "direct-participant",
"magnitude": 6, "probability": 0.30, "score": 1.8 },
{ "description": "Dose-response knowledge", "category": "knowledge",
"magnitude": 5, "probability": 0.80, "score": 4.0 }
],
"totalBenefitScore": 5.8,
"riskBenefitRatio": 4.35,
"justiceFactor": 0.9,
"isEthicallyAcceptable": true,
"recommendation": "approve",
"rationale": "Excellent risk-benefit profile with equitable distribution"
}
The risk scores are 0.10 × 6 = 0.6, 0.05 × 4 = 0.2 and 0.20 × 2 = 0.4, totalling 1.2; the benefit scores are 0.30 × 6 = 1.8 and 0.80 × 5 = 4.0, totalling 5.8. The unadjusted ratio is 5.8 / 1.2 ≈ 4.83, and multiplying by the justice factor 0.9 gives the stored 4.35. Under the SDK's thresholds (Chapter 5) that is an approve. Notice how much of the result comes from the "knowledge" benefit, which accrues to future patients rather than to the participant. A reviewer should read the category of each benefit, not just the total, because a favourable ratio built mainly on societal benefit is exactly the situation in which the Belmont principle of justice asks who bears the risk and who gains.
A final caution: the SDK has a second, unstructured way of describing risk. EthicalRiskAssessment (the input to assessEthicalRisk) takes potentialRisks and potentialBenefits as plain string arrays and counts them. Those strings are not Risk objects, have no probability or severity, and cannot be joined to a RiskBenefitAnalysis. When both are used in one system, generate the string lists from the structured records (for example, from each description) so that the two views describe the same risks.
GeneticData describes a dataset derived from one participant, not the sequence itself. Its fields are dataId, participantId, dataType (genome, exome, targeted-panel, snp-array or other), protectionLevel, an optional encryption descriptor, accessControls (a list of role or policy names), secondaryUseConsent, dataSharingConsent and returnOfResults.
protectionLevel | Spec level (§6.1) | Meaning | Implementation consequence |
|---|---|---|---|
anonymous | Level 1 | No identifiers; cannot be re-identified | Claim with care: a genome is itself an identifier, so true anonymity of whole-genome data is doubtful (Chapter 3) |
de-identified | Level 2 | Coded; re-identifiable with a key | The key must be held separately under its own access control (Chapter 7) |
confidential | Level 3 | Identified but protected | Strict role-based access and full access logging |
public | Level 4 | Openly shared | Requires explicit dataSharingConsent and a re-identification risk review before release |
The level numbers are labels, not a scale of protection strength: Level 1 data has the least identifying content, Level 3 the most, and Level 4 the least access restriction. Software should never compare levels numerically ("level ≥ 2") to make access decisions.
The consent flags must agree with the consent record. If secondaryUseConsent is true, the participant's InformedConsent must have futureUseDisclosed: true; a secondary-use flag that rests on a form which never disclosed future use is not consent at all. Because GeneticData has no consentId, this check requires the extension link recommended in 4.1. returnOfResults takes one of four policies — all, actionable, requested or none — which should match what the consent form told the participant would happen to findings.
An IncidentalFinding records one clinically relevant variant discovered outside the study's purpose. Its significance field uses the five-tier variant classification familiar from clinical genetics (pathogenic, likely pathogenic, uncertain significance, likely benign, benign — the scheme of the ACMG/AMP 2015 guideline). The remaining fields — actionable, clinicallyConfirmed, participantWantsToKnow, familyImplications, geneticCounselingProvided — are the inputs to the return decision of §6.3, and shouldReturn is its output.
The type stores shouldReturn as a plain boolean, so nothing prevents it contradicting the inputs. This book recommends deriving it rather than entering it by hand: return when significance is pathogenic or likely pathogenic, the finding is actionable, it has been clinically confirmed, and the participant has affirmatively asked to know (participantWantsToKnow === true; an absent value is not a yes, as in the classifyReturn function of Chapter 3). A variant of uncertain significance falls into the specification's "may return" band, which a boolean cannot express; if that distinction matters to the programme, extend the field to a three-valued should / may / should-not. Two further rules are cheap to enforce: a returned finding must have geneticCounselingProvided set, and a non-empty familyImplications should prompt a documented discussion of how relatives might be informed.
Gene-editing proposals are captured as a GeneEditingEvaluation — an input record — and evaluated into a GeneEditingDecision. The evaluation carries the two classification axes from Chapter 3 (editingType: somatic or germline; purpose: therapeutic, enhancement or research), a target, three booleans of scientific validity, four risk estimates, four ethical booleans, regulatoryCompliance and an optional internationalConsensus. Below is an illustrative somatic, therapeutic proposal; all numbers are assumptions for the example.
{
"evaluationId": "GEE-2026-005",
"editingType": "somatic",
"purpose": "therapeutic",
"target": "HBB-related sickle cell disease (ex vivo edited autologous cells)",
"scientificValidity": {
"preclinicalEvidence": true,
"mechanismUnderstood": true,
"safetyDemonstrated": true
},
"risks": {
"offTargetEffects": 0.2,
"immuneResponse": 0.3,
"mosaicism": 0.1,
"longTermUnknown": 0.6
},
"ethical": {
"consentObtained": true,
"benefitsOutweighRisks": true,
"noAlternatives": false,
"equitableAccess": false
},
"regulatoryCompliance": true,
"internationalConsensus": true
}
The four entries in risks are each on a 0–1 scale, but the type does not define what the numbers mean — probability of occurrence, expert-rated concern, or something else. The SDK averages them (here (0.2 + 0.3 + 0.1 + 0.6) / 4 = 0.3) and compares the mean with 0.5, as Chapter 5 details. An average hides the dominant term: in this example the long-term unknown (0.6) exceeds the threshold on its own, yet the mean is comfortably below it. Record the source of every estimate in a free-text rationale alongside the record, and have reviewers look at the maximum as well as the mean.
GeneEditingDecision holds decision (approve, approve-research-only, defer or reject), rationale, optional conditions, and lists of required oversight and monitoring. One mismatch between type and code needs handling here because it affects stored data: the reference SDK can return 'revise' on the somatic path, a value that is not in the decision union. A strict schema validator will refuse to store such a decision. Until the code is fixed, map 'revise' to 'defer' and keep its conditions, or widen the union explicitly.
Three record types document what happens once a study is running: adverse events, protocol deviations and audits.
| Record | Key fields | Enumerations |
|---|---|---|
AdverseEvent | eventId, participantId, studyId, description, onsetDate, resolutionDate?, actionsTaken[], reportedToIRB, reportDate? | severity: mild → moderate → severe → life-threatening → fatal; causality: unrelated, unlikely, possible, probable, definite; outcome: recovered, recovering, not-recovered, fatal, unknown |
ProtocolDeviation | deviationId, studyId, description, dateOccurred, correctiveActions[], reportedToIRB | category: major / minor; participantImpact: none, minimal, moderate, significant |
AuditResult | auditId, studyId, auditDate, auditor, findings[], complianceScore, recommendations[], followUpRequired | finding severity: critical, major, minor |
The five severity values resemble the grading used in oncology (CTCAE grades 1–5, from mild to death), and the five causality values follow the usual graded judgement of relatedness to the intervention. What the type lacks is just as important. Regulatory reporting turns on seriousness (an event that results in death, is life-threatening, requires or prolongs hospitalisation, causes persistent disability, or is a congenital anomaly, in the sense of ICH E2A) and on expectedness (whether the event is consistent with the known safety information). Severity is not seriousness: a severe headache may not be serious, and a moderate event requiring hospitalisation is. Because AdverseEvent has neither a seriousness nor an expectedness field, it cannot by itself determine whether an expedited report is required. This book recommends adding serious: boolean and expected: boolean as extension fields; Chapter 6 uses them to route reports under the applicable jurisdiction's rules.
{
"eventId": "AE-STUDY-2025-CANCER-0007",
"participantId": "P-12345",
"studyId": "STUDY-2025-CANCER",
"description": "Grade 3 neutropenia after cycle 2",
"onsetDate": "2026-02-03T08:00:00Z",
"resolutionDate": "2026-02-10T09:00:00Z",
"severity": "severe",
"causality": "probable",
"outcome": "recovered",
"actionsTaken": ["Dose held for cycle 3", "G-CSF administered"],
"reportedToIRB": true,
"reportDate": "2026-02-05T14:20:00Z"
}
In this illustrative record the event began on 3 February, was reported to the IRB two days later and resolved a week after onset. Consistency rules worth enforcing include: severity: 'fatal' if and only if outcome: 'fatal'; a resolutionDate not earlier than onsetDate, and present only when the outcome is recovered; a reportDate whenever reportedToIRB is true; and a participantId in the same pseudonymous form as in the consent record, so that the event can be joined to the consent the participant signed.
ProtocolDeviation records departures from the approved protocol. The major/minor category and the separate participantImpact scale let an IRB distinguish a late visit with no consequence from a missed safety test. A useful rule of thumb in validation is that any deviation with participantImpact of moderate or significant should not be categorised minor. Consent-related deviations — for example, a participant enrolled on an outdated form version — should reference the affected consentId, another extension field.
AuditResult summarises an audit as a list of findings, each with a category, description and severity, plus an overall complianceScore and followUpRequired. The type does not say how the audit score relates to the per-consent complianceScore; they are different measures that share a name. A consistent rule is to set followUpRequired whenever any finding is critical or major. Chapter 8 returns to audit scope and sampling.
The helper functions below implement several of the invariants discussed so far. They are deliberately small and independent of the SDK so that they can run at the point where records are written.
import type { InformedConsent, ConsentValidation, Risk, AdverseEvent } from './types';
export function toValidationInput(c: InformedConsent): ConsentValidation {
return {
participantId: c.participantId,
studyId: c.studyId,
consentForm: c.elements,
participantCapacity: c.participantCapacity,
comprehensionScore: c.comprehension?.score ?? 0, // never let the SDK default to 1.0
witnessPresent: c.witnessPresent,
signature: {
participant: c.signatures.participant.signed,
witness: c.signatures.witness?.signed,
lar: c.signatures.legallyAuthorizedRepresentative?.signed,
date: c.signatures.participant.date,
},
};
}
export function riskScoreMismatch(r: Risk, tol = 1e-6): boolean {
return Math.abs(r.probability * r.severity - r.score) > tol;
}
export function aeProblems(ae: AdverseEvent): string[] {
const p: string[] = [];
if ((ae.severity === 'fatal') !== (ae.outcome === 'fatal')) p.push('severity/outcome disagree on fatality');
if (ae.resolutionDate && ae.resolutionDate < ae.onsetDate) p.push('resolution before onset');
if (ae.reportedToIRB && !ae.reportDate) p.push('reportedToIRB without reportDate');
if (ae.reportDate && ae.reportDate < ae.onsetDate) p.push('report before onset');
return p;
}
toValidationInput derives the SDK's request type from the stored record. Note the explicit ?? 0: if the comprehension assessment is missing, the reference validator would otherwise assume a perfect score of 1.0, so the adapter makes a missing assessment fail instead of pass. riskScoreMismatch and aeProblems implement the checks from 4.4 and 4.6. None of these functions throws the standard's error codes itself; Chapter 5 shows how validation results are mapped to E001–E010.
The standard shows several identifier patterns without prescribing any. The specification example uses CONSENT-2025-001, P-12345 and STUDY-2025-CANCER. The reference SDK generates submission identifiers as IRB- + milliseconds since the epoch + nine random base-36 characters, and analysis identifiers as RBA- + milliseconds alone.
| Pattern | Source | Strength | Weakness |
|---|---|---|---|
CONSENT-YYYY-NNN | spec §8.3.1 example | Readable, sortable within a year | Needs a central counter; three digits overflow at 999 per year |
P-xxxxx | spec example | Short, pseudonymous | Must be allocated without deriving from real identifiers |
IRB-<ms>-<9 chars> | SDK submitIRBProtocol | Practically unique | Uses Math.random (not a cryptographic generator) and leaks submission time |
RBA-<ms> | SDK performRiskBenefitAnalysis | Simple | Two analyses in the same millisecond collide |
A robust pattern is to give every record an opaque, collision-resistant primary key (a random UUID, or a time-ordered identifier such as a ULID) and, where people need to read and quote identifiers, a separate human-friendly display code. The participant identifier deserves particular care: it must be a code assigned by the study, never a hospital record number, national identity number or anything derived from them by a reversible transformation. The mapping from code to person lives in a separate identity store, and that separation is what makes Level-2 ("de-identified") protection real.
The TypeScript types use Date, while the specification's JSON uses ISO 8601 strings in UTC with a trailing Z (2025-12-26T10:30:00Z). The two meet at serialisation: JSON.stringify turns a Date into an ISO string with milliseconds, but JSON.parse does not turn it back, so a record read from storage holds strings where the type promises Date objects. Comparisons such as "report date after onset date" then silently compare strings. Parse timestamps at the boundary, store them in UTC, and keep the local time zone of the site as a separate attribute if it is needed for display. The ordering rules scattered through this chapter — comprehension before consent, participant signature before or with the investigator's, onset before report — can then be checked reliably.
Two different versions are in play, and they should not be confused. The version field of InformedConsent is the version of the consent form text the participant saw ("3.1" in the example). Whenever the approved text changes, the version must change; when the change is material to the participant's willingness to continue — a new risk, for instance — participants already enrolled must be re-consented, producing new records that reference the new version. A version string alone does not prove which text was shown, so this book recommends storing a cryptographic hash of the rendered form alongside the version and language; Chapter 7 builds the audit trail around that hash.
The second version is that of the record schema itself. BIO-018 records carry no schema version, which becomes a problem the first time a field is added or renamed — as the three consent shapes show, that has already happened within version 1.0.0 of the standard. Adding an extension field such as schemaVersion: "WIA-BIO-018/1.0.0" to every stored record costs little and lets later software know which rules to apply when it reads an old record.
| Area | Issue in the published sources | Recommendation in this book |
|---|---|---|
| Consent elements | Three shapes (10 / 11 / 6 keys); validator counts true values | Canonical 10-field ConsentFormElements; normalise and reject unknown keys |
| Consent record | Spec example lacks capacity, voluntariness, witness flag | Populate every InformedConsent field; treat comprehension as mandatory |
| Links | No studyId on protocol or genetic records; no consentId on data | Add link fields as extensions |
| Risk records | score stored, not recomputed; ratio is benefit/risk | Recompute scores; store raw and adjusted ratio |
| Gene-editing decision | SDK can emit 'revise', outside the union | Map to 'defer' or widen the type |
| Adverse events | No seriousness or expectedness | Add serious and expected |
| Identifiers and time | Weak generated IDs; Date vs string | UUID/ULID keys; UTC ISO 8601 parsed at the boundary |
| Versioning | No schema version; form version not bound to text | Add schemaVersion; hash the rendered form |
With the records defined and their weak points understood, the next chapter turns to the SDK functions that read them — how validateInformedConsent, assessEthicalRisk, performRiskBenefitAnalysis and evaluateGeneEditingEthics compute their scores, and which of their behaviours an implementation must guard against.