Chapter 5

Chapter 5: The SDK and CLI: API Reference

What each function in the WIA-BIO-018 reference SDK and command-line tool actually computes, the thresholds it applies, where it disagrees with the specification, and how to wrap it safely.

Chapter 4 described the records that WIA-BIO-018 wants a system to keep. This chapter turns to the code that produces the numbers and recommendations written into those records: the TypeScript SDK in api/typescript/src/index.ts, its type file types.ts, and the shell tool cli/wia-bio-018.sh. The treatment is deliberately close to the source. For every public method the chapter gives the inputs, the arithmetic, the thresholds and the shape of the result, and it traces at least one call by hand so that a reader can check an implementation against the reference without running it.

Reading the reference implementation this closely has a second purpose. The SDK is version 1.0.0 and it has defects: some are harmless, a few can make a system report a consent as valid when it is not, and one crashes on an input the specification itself uses. None of them is hidden in this chapter. An IRB office that adopts the SDK should know exactly which answers it can rely on, and an engineer should know which ones need a wrapper. Section 5.8 collects the defects in one table. Every numeric result quoted in the chapter was reproduced by running the unmodified reference source (transpiled without type-checking) against the inputs shown.

One caution applies throughout. All thresholds and weights described here, such as the 0.8 comprehension minimum, the 0.30/0.30/0.20/0.15/0.05 weighting or the 1.5 multiplier for vulnerable populations, are design choices of WIA-BIO-018 and its SDK. They are not clinical or legal standards, and no regulation requires them. They make the SDK's behaviour predictable and testable; they do not make its output a verdict.

5.1 The SDK surface

One stateless class, four convenience functions

The SDK exports a single class, BioEthicsSDK. Its constructor takes no arguments and sets no configuration; the only state it holds is a version string returned by getVersion() ("1.0.0"). Every method is synchronous, performs no I/O and does not throw on ordinary input. The same input always produces the same output except for generated identifiers, which embed Date.now() and a random suffix. That makes the SDK easy to unit-test and easy to run inside any service, but it also means the SDK stores nothing: persisting results, attaching them to a consent-form version and writing the audit trail are left entirely to the calling system (Chapter 7).

Four methods are also exported as top-level functions that construct a fresh BioEthicsSDK and delegate to it: validateInformedConsent, assessEthicalRisk, submitIRBProtocol and evaluateGeneEditingEthics. The remaining public methods, performRiskBenefitAnalysis and assessEthicalPrinciple, are reachable only through an instance. The helper getEnhancedProtections, which returns the protection package for a vulnerable population, is declared private; callers cannot invoke it directly and see its output only as entries in requiredProtections.

Table 5-1. Public methods of BioEthicsSDK
MethodInput typeOutput typeKey rule
validateInformedConsentConsentValidationConsentValidationResultValid only if there are no violations and the weighted compliance score is at least 0.8
assessEthicalRiskEthicalRiskAssessmentEthicalRiskResultBenefit-to-risk ratio from list counts; approve / revise / reject
submitIRBProtocolIRBProtocolIRBSubmissionRoutes to exempt, expedited or full-board; status always submitted
evaluateGeneEditingEthicsGeneEditingEvaluationGeneEditingDecisionDecision tree over type × purpose, then evidence and consent checks
performRiskBenefitAnalysisstudyId, Risk[], Benefit[], justiceFactorRiskBenefitAnalysisΣbenefit ÷ Σrisk × justice factor, thresholds 2.0 / 1.5 / 1.0 / 0.7
assessEthicalPrincipleEthicalPrinciple, evidence, concernsPrincipleAssessment0.25 per evidence item, −0.2 per concern, clamped to 0–1
getVersionnonestringReturns "1.0.0"

The two smaller methods

submitIRBProtocol does not submit anything to anyone. It chooses a review level and wraps the protocol in an IRBSubmission whose status is always submitted and whose submissionId has the form IRB-<milliseconds>-<9 random base-36 characters>. The routing rule is short: a protocol at minimal risk with no entries in participants.vulnerablePopulations goes to exempt if its free-text studyType contains one of four phrases (educational research, anonymous surveys, observation of public behavior, existing de-identified data) and to expedited otherwise; a minor-increase protocol without vulnerable groups goes to expedited; everything else goes to full-board. Chapter 6 discusses how this compares with real regulatory categories. For this chapter it is enough to note that the exempt test is a case-insensitive substring match on free text, so it is only a first suggestion for the IRB office to confirm.

assessEthicalPrinciple scores one of the four principles from two string lists: each evidence item adds 0.25 (capped at 1.0 after four items) and each concern subtracts 0.2, with the result clamped to the range 0–1. The function never reads the strings. Four one-word evidence entries score exactly as well as four pages of documented safeguards. It is a counter, not an assessment, and the result should be stored only next to the evidence it counted.

Building the SDK from source

The package.json names the package @wia/bio-018 and the README shows npm install @wia/bio-018, but this book does not assume that a published package exists; treat the SDK as source that you build yourself. Before you do, be aware that the source as shipped does not pass tsc --noEmit (the typecheck script in package.json). Both files end with an export block that re-exports names already exported by their declarations, which TypeScript reports as conflicting exports, and evaluateGeneEditingEthics returns a decision value ('revise') that its own return type does not allow. The first problem is fixed by deleting the redundant trailing export blocks; the second is a real logic issue discussed in section 5.5.

5.2 Inside validateInformedConsent

Input and defaults

The method takes a ConsentValidation: participant and study identifiers, a consentForm object of booleans (declared as the ten-field ConsentFormElements), an optional participantCapacity, an optional comprehensionScore between 0 and 1, a witnessPresent flag and a signature object with booleans for participant, witness and lar. Two defaults matter a great deal. If participantCapacity is omitted the SDK assumes 'full', and if comprehensionScore is omitted it assumes 1.0, a perfect score. A caller that forgets to pass the comprehension result therefore gets the most favourable possible answer. A conformant wrapper should make both fields mandatory, or pass 0 when no assessment was recorded, as the example in section 5.6 does.

The checks, in order

The method collects four lists: violations, warnings, recommendations and required actions. It then applies the following checks.

  1. Form completeness. It counts the entries of consentForm whose value is true and divides by the number of entries. Anything below 1.0 is a violation that names the missing keys.
  2. Comprehension. A score below 0.8 is a violation that requires re-education and reassessment. A score of at least 0.8 but below 0.9 produces a warning and a recommendation for additional materials.
  3. Capacity. Capacity is converted to a number (Table 5-2). For diminished or severely-impaired participants, a missing LAR signature is a violation; a present one produces an informational warning. A borderline participant produces a warning recommending support and periodic reassessment.
  4. Witness required. If no witness was present and either capacity is not full or comprehension is below 0.85, that is a violation.
  5. Signatures. If neither the participant nor the LAR signed, that is a violation.
  6. Witness signature. If a witness was present but did not sign, that is a violation, but only when capacity is not full or comprehension is below 0.9. Note that this threshold (0.9) differs from the one used in step 4 (0.85).
Table 5-2. Components of the compliance score (SDK design values)
ComponentWeightHow the component value is obtained
Form elements0.30Share of consentForm entries that are true
Comprehension0.30The comprehensionScore as given (default 1.0)
Capacity0.20full 1.0 · borderline 0.85 · diminished 0.65 · severely-impaired 0.40
Signature0.151.0 if the participant or the LAR signed, otherwise 0
Witness0.051.0 if a witness was present and signed, or if no witness was needed (full capacity and comprehension ≥ 0.85); otherwise 0.5

The result is isValid = (violations.length === 0) && (complianceScore >= 0.8). The score also selects one closing recommendation: at least 0.95 is "excellent", at least 0.8 "good", at least 0.7 "needs improvement", and anything lower "inadequate — do not proceed". Because the score is a weighted average, it can be high while a violation still makes the consent invalid, and the two must never be read separately.

Three worked traces

The following traces use inputs derived from the consent record in spec §8.3.1 (comprehension 0.92 by teach-back, witness present and signed). They show how the same arithmetic behaves as single inputs change.

Table 5-3. Traced calls to validateInformedConsent (10-element form, all elements true)
CaseCapacity / comprehension / signaturesScore arithmeticResult
A. Baselinefull / 0.92 / participant + witness0.30 + 0.276 + 0.20 + 0.15 + 0.05 = 0.976valid, "excellent"
B. Just below the linefull / 0.79 / participant + witness0.30 + 0.237 + 0.20 + 0.15 + 0.05 = 0.937invalid: comprehension violation, despite a "good" score
C. LAR routediminished / 0.85 / LAR + witness, no participant signature0.30 + 0.255 + 0.13 + 0.15 + 0.05 = 0.885valid, with a comprehension warning and an LAR note

Case A reproduces the complianceScore of 0.98 printed in the spec's example record once 0.976 is rounded to two decimals (Chapter 4 checks the arithmetic); a system should store the unrounded value it actually computed and round only for display. Case B is the important one. A participant who answered 79% of comprehension items correctly produces a score of 0.937, comfortably above the 0.8 line, yet the consent is invalid because the comprehension check is a hard rule. Case C shows the LAR route working as intended: the participant does not sign, the representative does, the capacity weight drops the score to 0.885, and the consent remains valid.

Pitfall: the form check counts keys, not elements

Completeness is computed from whatever keys the caller passes. The README example sends the six-key form of spec §8.2.1 with every key true and no comprehension score; the SDK returns a perfect score of 1.0 and isValid: true, although four of the ten required disclosures, including future use of data and samples, were never checked. An empty consentForm is worse: zero divided by zero yields NaN, no violation is recorded, and the only reason the result is invalid is that NaN >= 0.8 is false. A wrapper should always build the full ten-field ConsentFormElements object itself and reject any input that lacks a key.

5.3 Inside assessEthicalRisk

Inputs and lookup tables

assessEthicalRisk gives a quick screening view of a study from six fields: a free-text studyType, a targetPopulation (either 'general-adults' or one of the nine VulnerablePopulation values), an interventionType, lists of potentialBenefits and potentialRisks, and a riskLevel. Two lookup tables translate the categorical inputs into numbers.

Table 5-4. SDK lookup values used by assessEthicalRisk
Intervention typeIntervention riskRisk levelBase risk
observational0.1minimal0.2
behavioral0.2minor-increase0.4
device0.5greater-than-minimal0.7
drug0.6high0.9
surgical0.8(unknown value)0.5
gene-therapy0.9
other or unknown value0.5

The arithmetic

multiplier   = (targetPopulation === 'general-adults') ? 1.0 : 1.5
totalRisk    = (baseRisk + interventionRisk) / 2 * multiplier + 0.05 * potentialRisks.length
benefit      = min(1.0, 0.2 * potentialBenefits.length)
ratio        = benefit / max(totalRisk, 0.1)          // a benefit-to-risk ratio
ethicalScore = min(1.0, ratio * 0.5)

Despite the field name riskBenefitRatio, the quantity is benefit divided by risk, so larger is better. The recommendation is then chosen by the first matching rule: approve if ethicalScore ≥ 0.7 and ratio ≥ 1.5; approve with a monitoring warning if ethicalScore ≥ 0.5 and ratio ≥ 1.0; revise if ethicalScore ≥ 0.3 or ratio ≥ 0.7; otherwise reject. Because ethicalScore is simply half the ratio (capped at 1), the double conditions collapse into single cut-offs on the ratio: approve at 1.5 or more, approve with warning from 1.0, revise from 0.6, reject below 0.6. Knowing this makes test design much simpler (Chapter 8).

Protections are then appended. The revise branch adds enhanced safety monitoring and a Data Safety Monitoring Board (DSMB). A greater-than-minimal or high risk level adds full IRB review, independent data monitoring and regular safety reviews. A gene-therapy intervention adds long-term follow-up "minimum 5 years", genetic counseling and a family-notification protocol. Any vulnerable population, or any risk level other than minimal, adds IRB approval with continuing review. Duplicates are removed at the end. The five-year figure is an SDK default and should not be read as sufficient: the US FDA's guidance on long-term follow-up after gene-therapy products recommends observation of up to 15 years for products such as integrating vectors and genome-editing products, and a program should follow the rule of its regulator.

A worked trace

Consider a hypothetical first-in-human study of a gene therapy in adult volunteers, recorded as interventionType: 'gene-therapy', riskLevel: 'greater-than-minimal', targetPopulation: 'general-adults', with three listed benefits and four listed risks.

Table 5-5. Step-by-step trace of one assessEthicalRisk call (illustrative inputs)
StepComputationValue
Base risk and intervention risklookup0.7 and 0.9
Population multipliergeneral-adults1.0
Averaged risk(0.7 + 0.9) / 2 × 1.00.8
List penalty4 × 0.050.2
Total risk0.8 + 0.21.0
Benefitmin(1, 3 × 0.2)0.6
Ratio0.6 / 1.00.6
Ethical score0.6 × 0.50.3
Rule matchednot ≥ 0.7; not ≥ 0.5; ethicalScore ≥ 0.3revise

The call returns nine protections: the DSMB and enhanced monitoring from the revise branch, three from the risk level, three from gene therapy and the continuing-review item. Two lessons follow. First, the study sits exactly on the revise/reject boundary. In floating-point arithmetic, 3 × 0.2 is 0.6000000000000001 and the ethical score becomes 0.30000000000000004, which is why the comparison with 0.3 succeeds. An implementation in another language that computes slightly differently, or rounds earlier, could return reject for the same study. Round scores to a fixed number of decimals before comparing, and write boundary tests. Second, listing a fifth risk, even a trivial one, would raise the total risk to 1.05, drop the ethical score below 0.3 and turn the result into reject. The score measures how many lines were written, not how serious they are.

The same method also shows the effect of the vulnerability multiplier. A drug study in children at minor-increase risk with two benefits and two risks gives (0.4 + 0.6) / 2 × 1.5 + 0.1 = 0.85 total risk, a benefit of 0.4, a ratio of about 0.47 and an ethical score of about 0.24, so the recommendation is reject, together with the child-specific protection list. The 1.5 multiplier is a blunt instrument: it treats every vulnerable group identically and applies whether or not the protections for that group are already in the protocol.

5.4 performRiskBenefitAnalysis

Where assessEthicalRisk counts list entries, performRiskBenefitAnalysis works with structured Risk and Benefit records (Chapter 4). It sums the score fields of each list, divides total benefit by the larger of total risk and 0.1, and multiplies the result by a caller-supplied justiceFactor between 0 and 1 (default 1.0). This is the ratio form of the principle calculation introduced in Chapter 1; the README's subtractive formula, (benefits − risks) × justice factor, does not match the code and should not be used.

The method never recomputes score. The types document it as probability × severity for a risk and probability × magnitude for a benefit, but the SDK trusts whatever number the caller put there. A wrapper should compute the score itself, or at least assert that the stored value matches the product, before calling the analysis.

Table 5-6. Recommendation rules of performRiskBenefitAnalysis (first match wins)
Adjusted ratioJustice factorRecommendation
≥ 2.0≥ 0.9approve ("excellent profile, equitable distribution")
≥ 1.5≥ 0.85approve ("favorable profile")
≥ 1.0≥ 0.8approve-with-modifications
≥ 0.7 or≥ 0.7defer
otherwisereject

Separately, isEthicallyAcceptable is true when the adjusted ratio is at least 1.0 and the justice factor at least 0.8. As an illustrative example, take one physical risk (probability 0.1, severity 6, score 0.6), one privacy risk (0.2 × 3 = 0.6) and one direct benefit (probability 0.4, magnitude 8, score 3.2), with a justice factor of 0.85. The raw ratio is 3.2 / 1.2 ≈ 2.67; adjusted, it is ≈ 2.27. That misses the first rule because the justice factor is below 0.9, matches the second, and the result is approve with isEthicallyAcceptable: true.

Two details catch people out. The returned field riskBenefitRatio holds the adjusted ratio, not the raw one, so a reviewer who wants to see how much the justice factor changed the outcome must recompute it from the totals, which the result also contains. And the defer rule uses or: a study with an adjusted ratio of 0.1 but a justice factor of 0.75 is deferred rather than rejected, because the justice test alone is enough to reach that branch. If an institution wants weak studies rejected regardless of distribution, it must add that rule in its own layer.

5.5 The evaluateGeneEditingEthics decision tree

The gene-editing evaluator encodes the somatic/germline × therapeutic/enhancement/research matrix discussed in Chapter 3. It takes a GeneEditingEvaluation containing the editing type and purpose, three scientific-validity booleans, four risk estimates between 0 and 1, and four ethical booleans. Its output is a GeneEditingDecision with a decision, a rationale and lists of conditions, oversight bodies and monitoring requirements.

germline
  ├─ enhancement  → reject   ("prohibited in most jurisdictions")
  ├─ therapeutic  → defer    (moratorium; 4 conditions, 4 oversight bodies,
  │                           multi-generational monitoring)
  └─ research     → falls through to the default below  ← defect
somatic
  ├─ enhancement                    → reject
  ├─ no preclinical evidence        → reject  ("complete preclinical studies")
  ├─ mechanism not understood       → add condition, continue
  ├─ safety not demonstrated        → defer   (IRB + DSMB)
  ├─ consent not obtained           → reject
  ├─ benefits do not outweigh risks → 'revise'  ← not a legal decision value
  ├─ avgRisk = mean of 4 risks; if avgRisk > 0.5 add mitigation condition
  │                                   and weekly monitoring for the first month
  ├─ therapeutic and avgRisk ≤ 0.5  → approve
  ├─ research                       → approve-research-only
  └─ otherwise                      → defer
default → defer ("insufficient information", ethics committee review)

On the approval paths the evaluator adds standard oversight (IRB approval with continuing review and an institutional biosafety committee, plus "FDA IND approval (or equivalent)" for therapeutic use) and standard monitoring (long-term follow-up "minimum 5 years", off-target surveillance, immune-response monitoring, efficacy assessment). If there are acceptable alternatives (noAlternatives false) it adds a condition to justify the choice, and if equitable access is not planned it adds a condition to develop such a plan. These are conditions on an approval, not grounds for refusal.

As an illustrative example, take a somatic therapeutic edit with preclinical evidence, understood mechanism, demonstrated safety, consent obtained and a favourable balance, risk estimates of 0.3, 0.2, 0.1 and 0.6 (mean 0.3), and no equity plan. The result is approve with one condition (develop an equitable access plan), three oversight entries and four monitoring entries. The same input marked purpose: 'research' returns approve-research-only with the added condition "No clinical application without separate approval".

Several properties of the tree need to be understood before its output is shown to a committee.

5.6 Error codes and the Result pattern

types.ts defines the enum BioEthicsErrorCode with ten codes and a class BioEthicsError that carries a code, a message and optional details. Spec §8.4 lists only the first six codes, with an action for each; E007 to E010 exist only in the type file. None of the SDK's methods creates a BioEthicsError: validateInformedConsent reports problems as English sentences in violations, and the other methods express problems through their recommendation or decision. The error codes are therefore a vocabulary that the integrating system must apply itself.

Table 5-7. WIA-BIO-018 error codes
CodeEnum nameActionTypical source in the SDK
E001INCOMPLETE_CONSENTComplete all required elements (§8.4)Form-completeness, signature and witness violations
E002INSUFFICIENT_COMPREHENSIONRe-educate and reassess (§8.4)Comprehension below 0.8
E003COERCION_DETECTEDRemove pressure, allow time (§8.4)None: voluntariness is not an input to the validator
E004CAPACITY_CONCERNSObtain LAR consent (§8.4)Diminished or severely impaired capacity without LAR
E005IRB_APPROVAL_REQUIREDSubmit protocol for review (§8.4)Caller's own check before enrolment
E006PRIVACY_VIOLATIONEnhance data protection (§8.4)None: raised by the privacy component (Chapter 7)
E007UNACCEPTABLE_RISKBook's recommendation: stop and revise the design; return to the IRBreject from assessEthicalRisk or performRiskBenefitAnalysis
E008VULNERABLE_POPULATIONBook's recommendation: add the population's protection package and route to full-board reviewA vulnerable population without its protections in the protocol
E009INVALID_GENE_EDITINGBook's recommendation: block clinical use; record the rationalereject from evaluateGeneEditingEthics
E010REGULATORY_NONCOMPLIANCEBook's recommendation: halt until the gap in the applicable regulation is closedregulatoryCompliance: false (not read by the SDK)

The type file also defines Result<T, E>, a discriminated union of { success: true; data: T } and { success: false; error: E }, and its promise form AsyncResult. No SDK method uses them, but they are a sensible shape for a wrapper: the caller cannot read data without first checking success, which forces the invalid path to be handled.

Example: validating the §8.3.1 consent record

The following wrapper takes a record in the shape of spec §8.3.1, converts its eleven-key elements object into the ten-field ConsentFormElements recommended in Chapter 4, calls the validator and returns a Result with coded errors. It does not rely on the SDK's defaults: a missing comprehension assessment is passed as 0.

import {
  validateInformedConsent, ConsentFormElements, ConsentValidationResult,
  BioEthicsErrorCode, BioEthicsError, Result,
} from '@wia/bio-018';   // or a relative path to your own build

interface SpecConsentRecord {            // the parts of §8.3.1 used here
  participantId: string;
  studyId: string;
  consentDate: string;                   // ISO-8601 UTC
  elements: Record<string, boolean>;
  participantCapacity?: 'full' | 'borderline' | 'diminished' | 'severely-impaired';
  comprehension?: { score: number; method: string; assessor: string };
  signatures: {
    participant: { signed: boolean };
    witness?: { signed: boolean };
    legallyAuthorizedRepresentative?: { signed: boolean };
  };
}

// 11 spec keys → 10 canonical fields. There is no spec key for future use.
function toFormElements(e: Record<string, boolean>): ConsentFormElements {
  return {
    purposeDisclosed:      e.purpose === true && e.procedures === true,
    durationExplained:     e.duration === true,
    risksDisclosed:        e.risks === true,
    benefitsExplained:     e.benefits === true,
    alternativesProvided:  e.alternatives === true,
    withdrawalRights:      e.voluntary === true && e.withdrawal === true,
    privacyProtection:     e.confidentiality === true,
    compensationExplained: e.compensation === true,
    contactProvided:       e.contact === true,
    futureUseDisclosed:    e.futureUse === true,
  };
}

function toCode(violation: string): BioEthicsErrorCode {
  if (violation.startsWith('Insufficient comprehension'))
    return BioEthicsErrorCode.INSUFFICIENT_COMPREHENSION;           // E002
  if (violation.includes('Legally Authorized Representative'))
    return BioEthicsErrorCode.CAPACITY_CONCERNS;                    // E004
  return BioEthicsErrorCode.INCOMPLETE_CONSENT;                     // E001
}

export function checkConsent(
  r: SpecConsentRecord,
): Result<ConsentValidationResult, BioEthicsError[]> {
  const witness = r.signatures.witness;
  const result = validateInformedConsent({
    participantId: r.participantId,
    studyId: r.studyId,
    consentForm: toFormElements(r.elements),
    participantCapacity: r.participantCapacity ?? 'full',
    comprehensionScore: r.comprehension?.score ?? 0,   // never default to 1.0
    witnessPresent: witness !== undefined,
    signature: {
      participant: r.signatures.participant.signed,
      witness: witness?.signed ?? false,
      lar: r.signatures.legallyAuthorizedRepresentative?.signed ?? false,
      date: new Date(r.consentDate),
    },
  });
  if (result.isValid) return { success: true, data: result };

  const errors = result.violations.map((v) => new BioEthicsError(toCode(v), v));
  if (errors.length === 0) {       // e.g. NaN or low score with no violation text
    errors.push(new BioEthicsError(BioEthicsErrorCode.INCOMPLETE_CONSENT,
      `complianceScore ${result.complianceScore} is below 0.8`));
  }
  return { success: false, error: errors };
}

Run against the §8.3.1 example exactly as printed, this wrapper returns success: false with a single E001 error, "Incomplete consent form. Missing: futureUseDisclosed". The failure is correct, not a bug in the wrapper: the spec's example record has no element for future use of data and samples, which is one of the ten disclosures of spec §3.1.1 and is especially important for biobanks (Chapter 2). Add a futureUse: true element, representing a form that actually discloses it, and the same record validates with a compliance score of 0.976. This is exactly the discrepancy that the three inconsistent consent shapes of Chapter 4 create, surfaced as a coded error rather than a silent pass.

Two design notes on the example. Matching English sentences to codes is brittle: if the SDK's wording changes, the mapping falls back to E001. It is acceptable for a thin wrapper around a pinned SDK version, but a longer-lived system should move the checks into its own code, or patch the SDK to return codes directly, and keep a regression test for each message. Second, the wrapper deliberately treats "witness present but signature missing" as E001 rather than inventing a code, because the standard defines none for it.

5.7 The command-line tool

cli/wia-bio-018.sh is a Bash script with four commands plus help and version. It needs bc for its floating-point comparisons. It is useful for demonstrations and training because it explains each step in coloured text, but it does not call the SDK: every rule is re-implemented in shell, and several are simplified or replaced with constants. Its output should not be recorded as the result of an ethics check.

Table 5-8. CLI commands, options and defaults
CommandOptions (default)What it really computes
validate-consent--participant (P-12345), --study (STUDY-001), --comprehension (0.85), --capacity (full), --witness (yes)Prints comprehension, capacity and witness checks, then a fixed compliance score of 0.9 and "Consent is VALID" regardless of the checks above
assess-risk--study-type (gene-therapy), --population (adults), --intervention (drug), --risk-level (minimal)Ratio = fixed benefit 0.7 ÷ intervention risk; ignores risk level and population in the ratio; uses the SDK's thresholds 1.5 / 1.0 / 0.7
submit-irb--protocol (protocol.json), --institution (a placeholder value), --risk-level (minimal), --vulnerable (no)Never reads the protocol file; never routes to exempt; prints hard-coded review timelines that do not come from the spec
evaluate-gene-editing--type (somatic), --purpose (therapeutic), --disease (sickle-cell)Type × purpose only; somatic therapeutic is always "APPROVE", with no evidence, safety or consent checks
version, helpnoneVersion 1.0.0 and usage text
$ wia-bio-018 validate-consent --comprehension 0.5 --capacity diminished --witness no
✗ Comprehension: FAIL (0.5 < 0.8)
⚠ Capacity: DIMINISHED - Needs LAR, can assent
✗ Witness: Required but not present
  Overall Compliance Score: 0.9
✓ Compliance: GOOD (0.9)
✓ Consent is VALID

The excerpt above (colour codes removed) shows the most serious CLI defect: two failed checks followed by "Consent is VALID". The same inputs passed to the SDK give an invalid result with three violations. Other differences are smaller but real. The README says to run wia-bio-018 --version, but the script recognises only the version command and exits with "Unknown command" for the flag. The help text lists only three capacity levels and three risk levels, although the script accepts the fourth capacity value. The population default adults matches the spec's §8.2.2 type but not the SDK, which expects general-adults; passing general-adults to the CLI falls into its generic "requires ethical review" branch. Gene-editing results also differ: the CLI defers all germline research with a "research only" note, while the SDK returns the generic default.

A practical rule follows: use the CLI to teach the vocabulary and the shape of the checks, and use the SDK, wrapped as in section 5.6, for anything that is stored. If a command-line interface is needed in production, build a thin one on top of the wrapper so that the terminal and the service can never disagree.

5.8 Known SDK defects and how to handle them

The following table lists the defects found while tracing the reference implementation for this chapter. Each one was reproduced with the unmodified source. The workaround column assumes a wrapper layer like the one in section 5.6; Chapter 8 turns each row into a regression test.

Table 5-9. Known defects in SDK 1.0.0
DefectSymptomWorkaround
Consent completeness counts the keys passedSix-key form passes at 1.0; an empty form yields NaN with no violationAlways build all ten ConsentFormElements fields; reject missing keys
Optimistic defaultsOmitted comprehension counts as 1.0; omitted capacity as fullMake both mandatory in the wrapper; pass 0 when no assessment exists
Risk and benefit scored by list lengthAdding a trivial risk line can turn revise into reject; content is never readShow the scores as screening aids only; base decisions on performRiskBenefitAnalysis with real probabilities and severities, reviewed by people
'adults' vs 'general-adults'The spec §8.2.2 and CLI value 'adults' is treated as vulnerable; the private protection lookup returns undefined and the call throws a TypeErrorNormalise 'adults' to 'general-adults' before the call; reject unknown populations
Germline research falls throughReturns generic defer, and the three prepared conditions are discardedHandle germline research in the wrapper and attach the conditions required by the governing rule
Somatic 'revise'Decision outside the declared union; compile error; skipped by exhaustive switchesMap to defer, keeping the rationale
Ignored gene-editing fieldsregulatoryCompliance and internationalConsensus are never readRefuse approval (E010) when regulatoryCompliance is false
OR in fall-back branchesJustice factor ≥ 0.7 alone yields defer instead of reject; revise in assessEthicalRisk is reached at ratio 0.6Add explicit institutional cut-offs above the SDK
Floating-point boundariesResults at 0.3, 0.8 or 1.5 depend on binary roundingRound to a fixed precision before comparing; test both sides of each boundary
Adjusted value stored under raw nameriskBenefitRatio in RiskBenefitAnalysis already includes the justice factorStore both raw and adjusted ratios in the audit record
Source does not type-checkDuplicate export blocks; the 'revise' valueRemove the trailing re-export blocks; fix the decision value
README examples use invalid valuesparticipantCapacity: 'competent' (becomes undefined capacity weight, so the score is NaN); interventionType: 'somatic-gene-editing' (falls back to 0.5); riskLevel omitted (falls back to 0.5); targetPopulation: 'adults' (throws)Do not copy README snippets; use the type definitions as the reference
CLI diverges from SDKFixed compliance score 0.9; fixed benefit 0.7; no exempt route; --version unsupportedUse the CLI for demonstration only

None of these defects changes the purpose of the SDK. Its value lies in making the standard's rules explicit and repeatable, so that two systems given the same consent record reach the same conclusion and can explain it. That value survives the defects as long as the integrating system knows about them, guards against them at its boundary, and keeps the human decision separate from the computed one. The thresholds and weights are the standard's proposals; the IRB remains the body that decides.

Key points of this chapter

Review Questions

  1. A participant with full capacity scores 0.84 on a quiz, a witness is present but does not sign, and all ten elements are disclosed. Compute the compliance score and decide whether the consent is valid. Which rule decides it?
  2. Why does a caller that omits comprehensionScore receive a more favourable result than one that passes 0.8? How should a wrapper prevent this?
  3. Show that the four recommendation rules of assessEthicalRisk reduce to cut-offs of 1.5, 1.0 and 0.6 on the ratio.
  4. For a behavioural study in prisoners at minimal risk with two benefits and one risk, trace assessEthicalRisk to its recommendation.
  5. A germline research proposal and a somatic research proposal enter evaluateGeneEditingEthics. What does each return, and why is the first result a defect?
  6. Which error codes can be derived from SDK output today, and which require checks the SDK does not perform? Give the source for each code's recommended action.
  7. List three ways in which the CLI's validate-consent command can disagree with the SDK for the same participant.