Chapter 5

Chapter 5: API, SDK and CLI — Implementing the Five Required Components

From the interfaces in spec §8.2 to the CellularTherapySDK class and the wia-bio-005 command-line tool: what each method computes, where it departs from the spec, and how to wrap it safely.

Chapters 2 and 3 described the laboratory side of WIA-BIO-005: how a batch is grown, which in-process tests it passes, and which release tests decide whether it may leave the facility. Chapter 4 gave the data shapes that carry those results. This chapter is where the numbers turn into running code. Spec §8.1 says that any conforming system must contain five components — a Dose Calculator, a Quality Assessor, a Manufacturing Tracker, a Safety Monitor and a Compliance Checker — and §8.2 sketches the interfaces for the first three. The reference implementation published with the standard is a TypeScript package (@wia/bio-005, version 1.0.0, MIT licence) whose main export is the class CellularTherapySDK, plus a Bash script, cli/wia-bio-005.sh, that install.sh copies onto the path as wia-bio-005.

We read both artefacts line by line and ran them on copies of the source. The results are reported here as they are, including the places where the implementation does something different from the spec, or something a clinical system should not rely on. That is deliberate: an integrator who knows exactly where the reference code stops can wrap it with a thin, auditable layer instead of forking it or trusting it blindly. Every clinical value quoted from the code — doses, drug names, monitoring intervals — is what the software encodes, not a treatment instruction. The current product label and the institution's own protocols always take precedence.

5.1 The API surface in the spec and how the SDK realises it

Spec §8.2 defines three pairs of TypeScript interfaces: DoseRequest/DoseResponse, QualityMetrics/QualityResult, and ManufacturingBatch. It is worth being precise about what kind of API this is. The spec describes in-process function signatures, not a network protocol. There is no normative URL, HTTP verb, authentication scheme or rate limit in WIA-BIO-005, and the SDK makes no network calls at all: every method is a pure, synchronous computation over the object you pass in (apart from reading the system clock for timestamps). If your architecture exposes the components as web services, the endpoint design, transport security and serialisation of Date fields are yours to specify, and they sit outside the standard.

The SDK keeps the spirit of §8.2 but not its letter. It widens several types, adds fields the spec does not mention, and renames one enumeration value. The table below lists the correspondences you will meet when you try to claim that a system "implements §8.2".

Table 5-1. Spec §8.2 interfaces versus the types in @wia/bio-005
Spec §8.2SDK typeDifferences an integrator must handle
DoseRequestDoseParametersstrategy is added and is required by the type ('weight-based' | 'flat-dose' | 'split-dose'); cellType accepts nine values (HSC, DC, Treg, Other added) instead of five.
DoseResponseDoseResultAdds targetMarkerCells, assessment, notes and a unit inside recommendedDose. The field dose is documented as cells/kg but holds a total count in flat-dose mode.
QualityMetricsQualityMetricsSame name, different shape: the spec's identity{targetMarker, contaminants} becomes identity{positive:{…}, negative:{…}} keyed by marker; cellCount and appearance are added and required.
QualityResultQualityResultSpec recommendation values are release | conditional | reject; the SDK returns release | conditional-release | reject | retest and adds a per-criterion criteria checklist.
ManufacturingBatchManufacturingProcessstage becomes currentStage and gains failed; startingMaterial, parameters and cpp (critical process parameters) are added; expansionRatio is an optional input.

Two practical consequences follow. First, a JSON document that validates against the spec's interfaces will not type-check against the SDK without an adapter, and vice versa; Chapter 4 gave the field mapping for the product record, and the same discipline applies here. Second, any external interface you publish should state which vocabulary it uses. A receiving system that expects conditional and is sent conditional-release will usually fall through to an "unknown value" branch, which in a release workflow is exactly where you do not want surprises.

Figure 5-1 places the five required components next to the SDK methods that implement them. Four components have code behind them. The fifth, the Compliance Checker, has none: the SDK contains no method that checks regulatory completeness, signatures, reporting deadlines or audit trails. Chapter 8 discusses what that component must verify; for now it is enough to know that you will be building it yourself.

The five required components of WIA-BIO-005 and the SDK methods that implement them Conforming system (spec §8.1) Dose Calculator calculateDose() throws C010 Quality Assessor assessQuality() assessPostThawQuality() generateCertificate() · C007 Manufacturing Tracker trackManufacturing() returns Alert[] Safety Monitor assessCRS() · assessICANS() monitorSafety() Compliance Checker no SDK method built by the integrator (Ch. 8)
Figure 5-1. Component diagram. Solid boxes are backed by CellularTherapySDK methods (each also exported as a standalone function where noted in Sections 5.2–5.5); the dashed box has no reference implementation.

The package also exports standalone wrappers — calculateDose, assessQuality, trackManufacturing, monitorSafety and generateCertificate — each of which simply constructs a new CellularTherapySDK and delegates. The class holds no state beyond a version string, so the two styles are interchangeable. assessCRS, assessICANS and assessPostThawQuality exist only as methods.

5.2 calculateDose: the Dose Calculator

5.2.1 Validation and the viable-cell step

calculateDose(params: DoseParameters) starts with three guards. A non-positive totalCells, a viability outside 0–100, or a non-positive patientWeight each throws a CellularTherapyError with code C010 (INVALID_PARAMETERS). These are the only input checks. The function does not reject a missing cellType, a targetMarkerPercent above 100, or a NaN weight (every comparison with NaN is false, so NaN passes the guard and propagates into the result). Validate such values before the call.

The calculation itself has two steps. Total viable cells are totalCells × viability / 100. If targetMarkerPercent is supplied, target-marker cells (for a CAR-T product, the CAR+ fraction) are totalViableCells × targetMarkerPercent / 100. Note what totalCells means here: the total nucleated count of the product, before viability is applied. The worked example in spec §6.1.1 describes its input as "2.5 × 10⁸ total viable cells, 95% viability" and then multiplies by 0.95 anyway. If your LIMS already reports a viable count, passing it as totalCells together with the viability applies the viability twice and understates the dose by 5–15% for typical products. Decide which count feeds the function, and name the field accordingly in your own interface.

5.2.2 Strategies and per-type ranges

With strategy: 'weight-based' (the default when the property is absent at run time), the dose is viable cells per kilogram, and the recommended range depends on cellType. For CAR-T only, the dose is recomputed from the target-marker cells when they are available, so the result is CAR+ cells/kg. For every other type, targetMarkerPercent is computed and returned but does not affect the dose. Any other strategy — flat-dose and, perhaps surprisingly, split-dose — falls into a single branch that returns total viable cells with a fixed range of 1 × 10⁸ to 6 × 10⁸ "total cells", whatever the cell type.

Table 5-2. Recommended ranges hard-coded in calculateDose
ConditionMinimumMaximumUnitSource in codeComment
weight-based, CAR-T1 × 10⁶5 × 10⁶cells/kg (CAR+ if given)CAR_T_DOSEConsistent with the label examples listed in spec §6.1.1.
weight-based, MSC1 × 10⁶2 × 10⁶cells/kgMSC_DOSEIgnores any marker percentage.
weight-based, CAR-NK1 × 10⁷1 × 10⁹cells/kgliteralMuch higher than doses reported in published CAR-NK studies; do not treat as typical (Section 5.8).
weight-based, TIL1 × 10⁹2 × 10¹¹cells/kgliteralSpec §2.1.2 states TIL doses as total cells; checking them per kg is a unit error (Section 5.8).
weight-based, any other type1 × 10⁶1 × 10⁷cells/kgliteral defaultApplies to iPSC, HSC, DC, Treg, Other; not grounded in the spec.
flat-dose or split-dose, any type1 × 10⁸6 × 10⁸total cellsliteralUses total viable cells, not CAR+ cells, even for CAR-T.

5.2.3 The assessment bands

After computing the dose, the function sets withinRange to min ≤ dose ≤ max and assigns one of four assessment labels: out-of-range below half the minimum, suboptimal between half the minimum and the minimum, optimal inside the range, acceptable above the maximum up to 1.5 times the maximum, and out-of-range again beyond that. The asymmetry deserves attention. A dose 40% above the recommended maximum is labelled acceptable while withinRange is false and notes advises dose reduction. A user interface that shows only the assessment word would present an over-range dose in reassuring language. Treat withinRange and notes as the primary signals and the label as secondary. When viability is below 85%, a further note is appended, but the dose is still returned.

5.2.4 Reproducing the spec's 70 kg example

Spec §6.1.1 works through one example: a 70 kg adult, 2.5 × 10⁸ cells, 95% viability, 80% CAR+. By hand, 2.5 × 10⁸ × 0.95 × 0.80 = 1.9 × 10⁸ CAR+ cells, and 1.9 × 10⁸ ÷ 70 = 2.71 × 10⁶ CAR+ cells/kg, inside the 1–5 × 10⁶ range. The SDK call and the printed result, captured from an actual run, are:

import { CellularTherapySDK } from '@wia/bio-005';

const sdk = new CellularTherapySDK();
const result = sdk.calculateDose({
  cellType: 'CAR-T',
  totalCells: 2.5e8,          // total nucleated cells in the bag
  viability: 95,              // %
  patientWeight: 70,          // kg
  targetMarkerPercent: 80,    // % CAR+ among viable cells
  strategy: 'weight-based',
});
console.log(JSON.stringify(result, null, 2));

// Output:
{
  "dose": 2714285.714285714,
  "totalViableCells": 237500000,
  "targetMarkerCells": 190000000,
  "withinRange": true,
  "recommendedDose": {
    "minimum": 1000000,
    "maximum": 5000000,
    "unit": "cells/kg"
  },
  "assessment": "optimal",
  "notes": []
}

The unrounded 2,714,286 CAR+ cells/kg matches the spec's rounded 2.7 × 10⁶. Two variations of the same call are instructive. Changing strategy to 'split-dose' returns dose: 237500000 with the unit total cells — all viable cells, not the 1.9 × 10⁸ CAR+ cells — and the result is still optimal against the 1–6 × 10⁸ range, even though no split has been calculated. Keeping 'weight-based' but passing targetMarkerPercent: 0 returns 3.39 × 10⁶ cells/kg, because the code tests if (targetMarkerCells) and zero is falsy; a product in which flow cytometry found no CAR+ cells is dosed as if every viable cell were CAR+. Both behaviours are covered by the wrapper advice in Section 5.8. The split fractions themselves (the 10/30/60% schedule) belong to the administration protocol in Chapter 6, not to this function.

5.3 assessQuality: the Quality Assessor

5.3.1 Pass/fail per criterion

assessQuality(metrics: QualityMetrics) evaluates five criteria against constants from CELLULAR_THERAPY_CONSTANTS: viability ≥ 85 (MIN_VIABILITY), potency ≥ 70 (MIN_POTENCY), sterility equal to true, endotoxin strictly below 5 (MAX_ENDOTOXIN), and mycoplasma equal to false. Mind the boolean polarity introduced in Chapter 4: sterility: true means "no growth, pass", while mycoplasma: false means "not detected, pass". A mapping layer that converts the spec's string "negative" into false for both fields will fail every sterile lot.

The function takes potency as a single percentage and does not know which assay produced it. Chapter 3 defined the spec's cytotoxicity criterion as specific lysis ≥ 70% at an E:T ratio of 10:1, and the constant 70 fits that reading. If your potency release test is a different assay (cytokine secretion, for instance, where the spec's criterion is a fold increase), feeding its raw result into this field compares it against the wrong scale. Decide per product which number goes in and record that decision in the product specification.

Two criteria in the returned checklist are not evaluated at all. The identity entry is always {required: 'Target markers present', actual: 'Verified', pass: true}; the identity object you pass in is never read. The cellCount and the appearance.color and appearance.clarity fields are also ignored; only appearance.particulates is used, and only to raise a warning.

5.3.2 Warnings, grade and recommendation

Three conditions produce warnings: viability that passes but is below 90%, endotoxin that passes but exceeds 80% of the limit (above 4), and visible particulates. The grade is then derived from failures and warnings. Any failure gives F. With no failures, the grade is A when there are no warnings and viability ≥ 95 and potency ≥ 85; otherwise B when there is at most one warning; otherwise C. The recommendation follows: if any failure is a sterility or mycoplasma failure, reject; if there are other failures (viability, potency, endotoxin), retest; with no failures, conditional-release when there are more than two warnings, otherwise release.

Because only three warnings exist, conditional-release is reached only when all three occur together. In practice the function almost always says release for a passing lot. That is a reasonable default for software, but it is not a disposition. The grade letters are an informative feature of this implementation; the spec lists the grade type in §8.2 without defining how it is computed, and the CLI computes it differently (Section 5.7). Nothing in WIA-BIO-005 makes "grade A" a regulatory category. Likewise retest is the SDK's choice for a viability, potency or endotoxin failure; spec §8.4 says "reject or investigate" for C001 and "reject or retest" for C002, leaving the decision to the quality unit.

5.3.3 Input and output

The following input describes a lot that passes with one warning. The output was captured from a run of the SDK source.

// Input: QualityMetrics
{
  "viability": 88.4,
  "potency": 78.5,
  "sterility": true,
  "endotoxin": 0.25,
  "mycoplasma": false,
  "identity": {
    "positive": { "CD3": 96.1, "CAR": 85.3 },
    "negative": { "CD19": 0.4 }
  },
  "cellCount": { "total": 520000000, "viable": 460000000 },
  "appearance": { "color": "pale yellow", "clarity": "slightly turbid", "particulates": false }
}

// Output: QualityResult
{
  "passRelease": true,
  "grade": "B",
  "failures": [],
  "warnings": [
    "Viability (88.4%) is acceptable but below optimal (≥90%)"
  ],
  "recommendation": "release",
  "criteria": {
    "viability":  { "required": 85,    "actual": 88.4,  "pass": true },
    "identity":   { "required": "Target markers present", "actual": "Verified", "pass": true },
    "potency":    { "required": 70,    "actual": 78.5,  "pass": true },
    "sterility":  { "required": true,  "actual": true,  "pass": true },
    "endotoxin":  { "required": 5,     "actual": 0.25,  "pass": true },
    "mycoplasma": { "required": false, "actual": false, "pass": true }
  }
}

Look closely at the endotoxin row. The value 0.25 is an EU/mL reading, which is also how the QualityMetrics comment labels the field, yet it is compared with 5 — and the constant is documented as MAX_ENDOTOXIN (EU/kg). The spec's release limit (§4.1.5) is fewer than 5 EU per kilogram of body weight for the dose, so the correct comparison requires the bag volume and the patient's weight, as derived in Chapter 3. With the same 0.25 EU/mL in a 50 mL bag for a 70 kg patient (illustrative figures), the dose carries 12.5 EU, or 0.18 EU/kg. The pass verdict happens to be the same here, but the arithmetic is wrong in general, and a small patient receiving a large volume can fail the per-kg limit while the SDK reports a pass. The same input with viability set to 84.9 returns grade F, recommendation retest and the single failure "Viability (84.9%) below minimum (85%)", which a caller would map to C001.

5.4 trackManufacturing: the Manufacturing Tracker

trackManufacturing(process: ManufacturingProcess) takes a snapshot of a batch in culture and returns a copy with new alerts appended and with cpp.withinRanges recomputed. It checks three of the critical process parameters from Chapter 2: temperature outside 36.5–37.5 °C and pH outside 7.2–7.4 each raise a critical alert; cell density outside 0.5–2.0 × 10⁶ cells/mL raises a warning. The constants for dissolved oxygen (20–40%) and osmolality (260–320 mOsm/kg) exist in CELLULAR_THERAPY_CONSTANTS.CPP, but the function never reads them. We confirmed this by passing a dissolved-oxygen value of 15% and an osmolality of 340 mOsm/kg: neither produced an alert.

The expansion rule compares a caller-supplied currentMetrics.expansionRatio with parameters.targetExpansion. It raises a warning only when the ratio is below half the target and more than 70% of the planned culture days have elapsed. For a ten-day culture with a 100-fold target, a batch at 40-fold on day 8 triggers the warning; the same batch on day 7 does not, because 7 is not greater than 7.0. The function does not compute the ratio from cell counts — that arithmetic belongs to the caller, as Chapter 2 showed. Viability in culture below 85% is critical; 85% up to but not including 90% is a warning.

Finally, withinRanges is set to true only if no unresolved critical alert remains in the list — including alerts that were already in the input. That has two effects you need to design for. The function appends rather than replaces, so if you feed its output back in every hour, the same pH excursion will be recorded again every hour; deduplicate by alert type and open state before persisting. And a critical alert keeps withinRanges false until someone sets resolved: true with a resolution note, which is exactly the human acknowledgement a deviation process (C006) needs. Alert identifiers are built from a prefix and Date.now(), so they are unique per type per millisecond but not globally unique; use your own identifiers for anything stored. The function never changes currentStage: stage transitions and their gate checks are the protocol subject of Chapter 6.

// Day 8 of a planned 10-day culture (illustrative values)
const out = sdk.trackManufacturing({
  ...batch,                                     // batchId, cellType, dates, startingMaterial…
  parameters: { medium: 'X-VIVO 15', cytokines: ['IL-2'], moi: 5,
                targetExpansion: 100, cultureDays: 10 },
  currentMetrics: { cellCount: 4e9, viability: 88, dayInCulture: 8, expansionRatio: 40 },
  alerts: [],
  cpp: { temperature: 37.0, pH: 7.1, dissolvedOxygen: 15,
         cellDensity: 1.2e6, osmolality: 340, withinRanges: true },
});
// out.alerts (severity, message):
//   critical  pH out of range: 7.1 (acceptable: 7.2-7.4)
//   warning   Expansion ratio (40.0×) below target (100×) at day 8
//   warning   Viability (88%) below optimal (≥90%)
// out.cpp.withinRanges === false
// (dissolved oxygen 15% and osmolality 340 mOsm/kg raised nothing)

5.5 Safety and cryopreservation helpers

5.5.1 assessCRS and assessICANS

Both methods accept an assessment object (CRSAssessment or ICANSAssessment, described in Chapter 4) and return {grade, treatment, urgency}. The key fact is that neither method grades anything. The grade is read from the input and echoed back; the temperature, hypotension, vasopressor, oxygen, ICE-score, consciousness and seizure fields are not consulted. The grade must therefore come from a clinician or from your own implementation of the ASTCT 2019 consensus criteria, which Chapter 7 presents in full. For ICANS, note also an erratum inherited from the spec: iceComponents allows 0–2 points for following commands and has no attention item, whereas the ASTCT ICE score gives 1 point for commands and 1 for attention. Store the ASTCT components in your own record and compute the total there.

What the methods add is a fixed urgency mapping — grades 0 and 1 routine, grade 2 urgent, grades 3 and 4 emergency — and a fixed treatment string per grade that names drugs and doses. Those strings are English text constants written into version 1.0.0 of the code. They are not linked to a formulary, do not reflect the patient's weight, and will not change when a product label is updated. Show them, if at all, as reference text beside the institutional protocol, never as an order or a pre-filled prescription. The urgency value is the more useful output: it can drive who is paged and how quickly.

5.5.2 monitorSafety

monitorSafety(patientId, daysPostInfusion, events) filters the SafetyEvent list to events without a resolutionDate and rolls them up into a risk level. Any active event of severity 4 or higher gives critical; otherwise any active event flagged serious, or CRS and ICANS active together, gives high; otherwise any active event gives moderate; no active events gives low. The patientId argument is accepted but unused.

The recommendations are string lists assembled from two sources: the day count (up to day 10, inpatient monitoring and CRS/ICANS assessment every 4–6 hours; days 11–30, staying within two hours of the treatment centre with weekly visits; after day 30, nothing day-based) and the types of the active events (CRS, ICANS, infection, cytopenia). The day-based text reproduces the spec's historical defaults; as Chapter 7 explains, current regulatory practice for approved autologous CAR-T products uses a shorter proximity period, and a fixed inpatient period is not a regulatory requirement. Treat these outputs as prompts for the care team, configured per product and site.

5.5.3 assessPostThawQuality

This method compares pre-freeze and post-thaw measurements. It returns post-thaw viability, recovery (post-thaw count ÷ pre-freeze count × 100), potencyRetained (post-thaw potency ÷ pre-freeze potency × 100) and acceptable, which requires viability ≥ 85, recovery ≥ 70 (MIN_RECOVERY) and potency retained ≥ 70. For example, pre-freeze {94%, 2.5 × 10⁸ cells, potency 80} and post-thaw {87%, 1.9 × 10⁸, potency 62} yield recovery 76% and potency retained 77.5%: acceptable. A zero pre-freeze count or potency produces Infinity or NaN, and NaN makes acceptable silently false, so reject such inputs explicitly. The optional timeToInfusion field of PostThawQuality is never filled; the thaw-to-infusion window of Chapter 6 must be tracked elsewhere.

5.5.4 generateCertificate

generateCertificate(product, quality, potency) runs assessQuality internally and throws C007 (QUALITY_FAILURE) when passRelease is false. Otherwise it returns a CertificateOfAnalysis with a number of the form COA-<batchId>-<milliseconds>. Several fields in that object are hard-coded placeholders rather than data: approved: true and approvedBy: 'QC Manager', a facility of 'GMP Manufacturing Suite', an empty deviations list, an expiry date 24 months after the freeze date (or after the moment of generation when no storage record exists), and shipping conditions of 'Dry ice or liquid nitrogen vapor phase'. The last one is unsafe: dry ice sits near −78 °C, far warmer than the ≤ −150 °C storage the spec requires, and a cryopreserved cell product must not be shipped on dry ice. Use the generator as a draft template whose placeholders QA overwrites with the real approver, facility, deviation list, validated shelf life and a validated vapour-phase liquid-nitrogen shipper. A certificate is signed by a person, not produced by a function call.

5.6 The error model: C001–C010

Spec §8.4 defines six error codes, C001 to C006, each with a meaning and an action. The SDK enumeration CellularTherapyErrorCode keeps those six and adds four more, C007 to C010, which are an implementation extension rather than part of the normative spec. Errors are thrown as CellularTherapyError, a subclass of Error with a code and an optional details record (never populated by the SDK itself; use it in your own wrappers for the batch or patient context).

The most important point about this table is the fourth column. The SDK throws only two of the ten codes: C010 from calculateDose and C007 from generateCertificate. Every other code has to be raised by your integration layer when it interprets SDK results or its own checks. A system that waits for the SDK to throw C003 before blocking a contaminated lot will wait forever.

Table 5-3. Error codes, where they come from, and the operational action
CodeEnum nameDefined inThrown by SDK?Typical trigger in an integrationOperational action
C001VIABILITY_LOWSpec §8.4NoViability criterion failed in assessQuality or post-thaw checkHold the lot; reject or investigate (spec); QA decides on retest
C002POTENCY_FAILURESpec §8.4NoPotency criterion failedReject or retest per the product specification
C003STERILITY_FAILURESpec §8.4NoSterility (and, by the SDK's rule, mycoplasma) failure; SDK recommendation rejectReject immediately; notify QA and, if any product has left the site, the treating team
C004DOSE_OUT_OF_RANGESpec §8.4NowithinRange === false from calculateDoseRecalculate; physician and pharmacy review before any dispensing
C005IDENTITY_MISMATCHSpec §8.4NoProduct and patient identifiers disagree at any hand-off, or the identity test failsStop and investigate; do not infuse
C006MANUFACTURING_DEVIATIONSpec §8.4NoUnresolved critical alert from trackManufacturingDocument the deviation and assess impact (CAPA)
C007QUALITY_FAILURESDK onlyYes, generateCertificateCertificate requested for a lot with passRelease === falseNo CoA; route to QA disposition
C008STORAGE_VIOLATIONSDK onlyNoStorage temperature above the ≤ −150 °C requirement, or an unplanned freeze–thaw cycleQuarantine; assess against stability data
C009EXPIRED_PRODUCTSDK onlyNoCurrent date after StorageInfo.expirationDateBlock dispensing
C010INVALID_PARAMETERSSDK onlyYes, calculateDoseNon-positive cell count or weight, viability outside 0–100Correct the input at source; never substitute a default

A useful discipline is to treat each code as an event with an owner. C001–C003 and C007 belong to the quality unit, C006 to manufacturing, C008 and C009 to whoever holds the cryo-inventory, C004 and C005 to the clinical site. Logging the code together with the batch identifier, the rule and threshold that fired, and the person who acknowledged it gives you the audit trail that Chapter 8 will require. Because catch blocks see all errors, check err instanceof CellularTherapyError before reading err.code, and let unexpected errors propagate rather than mapping them to C010.

5.7 The CLI: wia-bio-005

The command-line tool is a self-contained Bash script. It does not call the SDK; it re-implements a subset of the logic using bc for arithmetic. It offers four working commands plus version and help. Unknown options are skipped silently, and flags take the defaults shown below when omitted.

Table 5-4. CLI commands, flags and defaults
CommandFlags (default)What it does
calc-dose--cells (5e8), --weight (70), --viability (92.5), --car (0 = none)Viable cells, CAR+ cells, cells/kg; checks only the CAR-T range 1–5 × 10⁶/kg. No cell-type or strategy flag.
assess-quality--viability (92.5), --potency (85), --endotoxin (0.3), --sterility (true), --mycoplasma (false)Five pass/fail checks, a grade and a recommendation (no identity, no particulates).
track-batch--batch (CT-2024-001), --stage (expansion), --day (7), --cells (5e7), --viability (92)Prints the next stage, a viability band and, in expansion, a ratio against a fixed start of 5 × 10⁶ cells.
monitor-safety--patient (PT-001), --days (7), --crs (0), --icans (0)Echoes grades with short treatment text and day-based monitoring advice.
version, help—help also answers to --help and -h; --version is not recognised.

The README shows monitor-safety --day 7; the flag is --days, and because unknown options are ignored, --day 7 quietly runs with the default of 7 — right only by coincidence. The installer suggests wia-bio-005 --help, which works, while the README's wia-bio-005 --version exits with "Unknown command". The session below was captured from the script as published (colour codes and the banner removed). Cell counts are written as plain integers for a reason explained in Section 5.8.

$ wia-bio-005 calc-dose --cells 250000000 --weight 70 --viability 95 --car 80
▶ Dose Calculation
  Total Cells: 2.50e+08
  Patient Weight: 70 kg
  Viability: 95%
  CAR+ Percentage: 80%
  Viable Cells: 2.38e+08
  CAR+ Cells: 1.90e+08
▶ Results
✓ Dose: 2.71e+06 cells/kg
✓ Dose is within recommended range (1e6 - 5e6 cells/kg)

$ wia-bio-005 monitor-safety --patient PT-001 --days 14 --crs 2 --icans 0
▶ Toxicity Assessment
  CRS Grade: 2
⚠ CRS Grade 2: Hypotension and/or hypoxia
  Treatment: Tocilizumab 8 mg/kg IV
  ICANS Grade: 0
✓ ICANS: None
▶ Monitoring Recommendations
  Patient should remain within 2 hours of center
  Weekly clinic visits required
⚠ Active Grade ≥2 toxicity - urgent intervention required

$ wia-bio-005 assess-quality --viability 92.5 --potency 85 --endotoxin 0.3
▶ Release Criteria
✓ Viability: PASS (≥85%)
$ echo $?
1

$ wia-bio-005 calc-dose --cells 2.5e8 --weight 70 --viability 95 --car 80
  Viable Cells: 0.00e+00
(standard_in) 1: syntax error
✓ Dose: 0.00e+00 cells/kg
⚠ Dose exceeds recommended maximum (5e6 cells/kg)

The first two commands behave as intended. The last two do not, and Section 5.8 explains why. Beyond those defects, keep three design differences in mind when the CLI and SDK are used side by side. The CLI grades on viability and potency bands (A at ≥ 95% and ≥ 85%, B at ≥ 90% and ≥ 80%, otherwise C), while the SDK grades on warning counts, so the same lot can receive different letters: with viability 88.4% and potency 78.5%, the SDK returns B and a repaired CLI returns C. The CLI never returns conditional-release. And the CLI's messages describe endotoxin as "< 5 EU/mL", repeating the unit confusion of Section 5.3. Use the CLI for demonstrations and quick checks, not as a release tool.

5.8 Known implementation gaps and how to wrap them

None of the issues below requires forking the reference code. Most can be neutralised by a thin wrapper that validates inputs, supplies what the SDK omits, and re-labels outputs. Keeping the SDK unmodified has a real benefit: when a corrected release appears, you can upgrade and delete wrapper code, and your validation evidence (Chapter 8) stays tied to a published version.

Table 5-5. Gaps found in @wia/bio-005 1.0.0 and the CLI, with mitigations
AreaObserved behaviourMitigation in your layer
Module exportsindex.ts ends with export { CellularTherapySDK } after export class CellularTherapySDK; types.ts re-exports already-exported names. Loading the source with Node.js type stripping failed with a SyntaxError at the duplicate export.Build from a copy with the redundant trailing export lines removed, and use import type for type-only imports.
Endotoxin unitsAn EU/mL value is compared with a limit of 5 documented as EU/kg.Compute EU/kg from concentration, dose volume and weight; judge against the route's limit (5 EU/kg for intravenous; much lower, 0.2 EU/kg, for intrathecal).
Identity checkAlways pass: true; input ignored.Evaluate the identity limits of Chapter 3 yourself and replace the criterion.
CAR+ = 0Falls back to all viable cells.Reject a missing or zero CAR+ result for CAR-T before calling.
Split and flat dosesplit-dose equals flat; flat uses all viable cells, not CAR+.Compute the total CAR+ dose yourself; derive fractions in the protocol layer (Chapter 6).
Per-kg TIL and CAR-NK rangesTIL total-dose range checked per kg; CAR-NK range far above published experience.Take ranges from the product label or protocol through configuration; ignore the SDK's range for these types.
Unchecked CPPsDissolved oxygen and osmolality never evaluated.Add both checks using the same constants.
Alert accumulationOutput alerts are appended to input alerts on every call.Deduplicate open alerts by type before storage.
CoA placeholdersApprover, facility, deviations, 24-month expiry and dry-ice shipping are constants.Overwrite every placeholder from QMS data; never ship on dry ice.
CLI arithmeticbc cannot parse 5e8: the default calc-dose prints a zero dose and then the wrong warning, and the expansion ratio in track-batch is always 0.0 because its hard-coded start value is 5e6.Pass plain integers; convert e-notation before calling bc.
CLI exitWith set -e, ((pass_count++)) returns status 1 when the counter is 0, so assess-quality stops after the first PASS with exit code 1.Use pass_count=$((pass_count+1)) (and the same for fail_count); with that change the full report prints.

The wrapper below shows the pattern for the two most consequential quality gaps, endotoxin and identity. It lets the SDK judge the criteria it handles correctly, then replaces the two it does not. The per-route limits are the general pharmacopoeial values mentioned in Chapter 3; confirm the limit that applies to your product in its specification.

import {
  CellularTherapySDK, QualityMetrics, QualityResult,
} from '@wia/bio-005';

type Route = 'intravenous' | 'intrathecal';
const ENDOTOXIN_LIMIT_EU_PER_KG: Record<Route, number> = {
  intravenous: 5,
  intrathecal: 0.2,
};

interface ReleaseContext {
  endotoxinEUperML: number;   // LAL result on the final product
  doseVolumeML: number;       // volume that will be infused
  patientWeightKg: number;
  route: Route;
  identityPass: boolean;      // evaluated against Chapter 3 identity limits
}

export function assessRelease(
  sdk: CellularTherapySDK, metrics: QualityMetrics, ctx: ReleaseContext,
): QualityResult {
  if (!(ctx.doseVolumeML > 0 && ctx.patientWeightKg > 0)) {
    throw new Error('volume and weight must be positive numbers');
  }
  const euPerKg = (ctx.endotoxinEUperML * ctx.doseVolumeML) / ctx.patientWeightKg;
  const limit = ENDOTOXIN_LIMIT_EU_PER_KG[ctx.route];

  // Let the SDK judge viability, potency, sterility and mycoplasma only.
  const base = sdk.assessQuality({ ...metrics, endotoxin: 0 });

  const failures = [...base.failures];
  const warnings = [...base.warnings];
  const endoPass = euPerKg < limit;
  if (!endoPass) failures.push(`Endotoxin ${euPerKg.toFixed(3)} EU/kg ≥ ${limit} EU/kg`);
  else if (euPerKg > 0.8 * limit) warnings.push('Endotoxin above 80% of limit');
  if (!ctx.identityPass) failures.push('Identity criteria not met');

  const pass = failures.length === 0;
  return {
    ...base,
    passRelease: pass,
    grade: pass ? base.grade : 'F',     // informative only
    failures,
    warnings,
    recommendation: pass ? base.recommendation
      : base.recommendation === 'reject' ? 'reject' : 'retest',
    criteria: {
      ...base.criteria,
      endotoxin: { required: limit, actual: euPerKg, pass: endoPass },
      identity: { required: 'Chapter 3 identity limits',
                  actual: ctx.identityPass ? 'met' : 'not met',
                  pass: ctx.identityPass },
    },
  };
}

The same approach handles dosing: validate that a CAR-T call carries a positive CAR+ percentage, call calculateDose in weight-based mode, and, for flat or split dosing, compute the CAR+ total yourself and compare it with a range loaded from configuration that cites the product label. When the wrapper overrides an SDK value, record both values and the reason in the audit log so that a reviewer can see what the reference code said and why your system disagreed.

Chapter summary. Spec §8.2 defines function-level interfaces, not a web API, and @wia/bio-005 implements four of the five required components with somewhat different types. calculateDose reproduces the spec's 70 kg example exactly (2.71 × 10⁶ CAR+ cells/kg) but mishandles zero CAR+, split dosing and some per-type units. assessQuality judges five criteria, compares endotoxin on the wrong scale and never checks identity. trackManufacturing checks three of the five CPPs. The safety helpers echo grades rather than compute them. Only C007 and C010 are ever thrown; the other eight codes are yours to raise. The CLI is a demonstration tool with two defects that break its default runs. Wrap the SDK, do not fork it, and keep a clinician or the quality unit as the decision-maker.