Chapter 5

Chapter 5: API — The @wia/bio-010 SDK and CLI

What each call in the reference TypeScript package and the shell tool promises, what it actually does when you run it, and how to use it safely while its known defects remain.

Chapters 2 to 4 described the data and the statistics of a clinical trial in terms of the standard: formulas from spec §4, JSON objects from §7, safety rules from §5. This chapter turns to the software that ships with WIA-BIO-010 v1.0. Two artifacts sit in the repository next to the specification: a TypeScript package named @wia/bio-010 (version 1.0.0, MIT licence) under api/typescript/, and a Bash script cli/wia-bio-010.sh that install.sh copies to /usr/local/bin/wia-bio-010. Spec §8.2 describes three interfaces — register a trial, collect patient data, calculate statistics — and the package is the reference implementation of those interfaces plus several more.

A reference implementation is useful precisely because it can be run. Everything reported in this chapter was obtained by reading the source and executing it: the TypeScript files were compiled and each method was called with the inputs shown, and the CLI was run command by command. Where the behaviour differs from the specification, from the documentation in README.md or from sound clinical-data practice, the chapter says so and gives a workaround. The aim is not to discourage use of the package. It is to let you decide, call by call, whether a result can be taken as it is, needs a guard around it, or must be replaced by another tool.

One point of vocabulary. In this chapter an API means the programming interface of the library: its functions, their parameter types and their return types. The package does not include an HTTP server, and v1.0 does not define a network endpoint. If your organization exposes these operations as a web service, the request and response shapes described here are a good starting point for that service's contract, but the transport, authentication and persistence are yours to design (Chapter 7).

5.1 Package layout and entry points

What is in the repository

The package directory contains three files: package.json, src/index.ts and src/types.ts. The manifest describes a build with tsup into CommonJS and ES module bundles under dist/, a Jest test configuration, ESLint and Prettier settings, and a ./types subpath export so that type-only consumers can import from @wia/bio-010/types. None of the generated files are committed: there is no dist/, no test file matching the Jest pattern, and neither the README.md nor the LICENSE listed under files exists inside the package directory. You therefore build the package yourself before using it, and you should not assume that a published registry version exists or matches the repository.

Building it exposes the first defect. Both source files end with a second export block that re-exports names already exported where they are declared (export { ClinicalTrialSDK } in index.ts; export type { ClinicalTrial, … } and export { CLINICAL_CONSTANTS, … } in types.ts). A current TypeScript compiler rejects this with "Cannot redeclare exported variable" and "Export declaration conflicts with exported declaration" errors, so the typecheck script fails as shipped. A third error comes from collectPatientData, which destructures hba1c from laboratoryResults.chemistry even though the chemistry type does not declare that field. Deleting the two trailing export blocks and adding hba1c?: number to the chemistry type makes the package compile cleanly under --strict; the examples in this chapter were run against a copy patched in exactly that way and in no other.

Two ways in: the class and the standalone functions

All functionality lives in the class ClinicalTrialSDK, which is also the default export. The constructor takes no arguments and holds no state beyond a version string; getVersion() returns "1.0.0". Alongside the class, index.ts exports seven standalone functions — registerTrial, collectPatientData, recordAdverseEvent, calculateSampleSize, calculatePower, calculateStatistics and generateSubmission — each of which creates a fresh instance and delegates to it. Because the class is stateless, the choice between the two styles is not about performance. It matters because several standalone functions have narrower signatures than the methods they wrap, as the reference table below shows. Where they differ, prefer the class.

types.ts also declares a generic Result<T, E> union ({ success: true; data: T } | { success: false; error: E }) and its promise form AsyncResult. No function in the package returns either. Every method is synchronous and signals failure by throwing ClinicalTrialError, except collectPatientData, which reports validation failures through a success flag in its own response type. When you wrap the SDK in a service, pick one of these conventions for your own layer and convert at the boundary.

API reference

Method (class)InputOutputThrowsStandalone difference
registerTrialobject: title, phase, indication, primaryEndpoint (string), sampleSize, optional duration, sponsor, investigatorClinicalTrialnothingno sponsor or investigator in the parameter type
enrollPatienttrialId, demographics, consentDatePatientEnrollmentnothingno standalone version
collectPatientDataDataCollectionRequestDataCollectionResponsenothingsame
recordAdverseEventOmit<AdverseEvent, 'aeId'>AdverseEvent plus reportingRequired, reportingDeadlinenothingdeclared return type is plain AdverseEvent
calculateSampleSizeSampleSizeCalculationSampleSizeResultB005same
calculatePowerPowerCalculationPowerResultnothingsame
performHypothesisTestnumber[], number[], alpha = 0.05HypothesisTestResultB005 if a group is emptyno standalone version
calculateDescriptiveStatsnumber[]DescriptiveStatsB005 if emptyno standalone version
generateSubmissiontrialId, agency, format, type, datasetsRegulatorySubmissionnothingtakes only trialId, agency, format; forces type: 'OTHER' and empty datasets
—calculateStatistics exists only as a standalone function; it dispatches to power or sample size (section 5.5)—

Two observations follow from the "Throws" column. First, only the statistics methods ever throw, and only with code B005. Second, the package never persists anything: identifiers are generated, objects are returned, and nothing is written anywhere. Every storage, audit-trail and access-control duty listed in spec §3.4 falls on the application that calls the SDK.

5.2 Trial and subject APIs

registerTrial

registerTrial turns six to eight flat parameters into the nested ClinicalTrial object introduced in Chapter 3. The string passed as primaryEndpoint becomes primaryEndpoint.parameter; the metric is always set to "Change from baseline" and the timepoint to "Week duration". The planned sample size is copied in, enrolled/randomized/completed/withdrawn counters start at zero, and dropoutRate is set to the constant 0.20 regardless of what the protocol assumes. The status starts at planned, which is the first state of the trial state machine described in Chapter 6.

FieldValue producedComment
id"NCT" + last 8 digits of the current time in millisecondsLooks like a ClinicalTrials.gov number but is not one; two calls in the same millisecond collide
duration52 (weeks) if omittedAlso drives the endpoint timepoint and the completion estimate
sponsor"Not specified" if omittedA placeholder string is harder to detect than a missing value
principalInvestigator{ name: "Principal Investigator", institution: "Research Center" } if omittedSame concern
startDate, registrationDatenowThe start date of a trial is normally the first-subject date, not the registration date
estimatedCompletionDatenow + duration weeksIgnores the recruitment period
protocolVersion"1.0"Not settable through the method

The identifier deserves emphasis. A National Clinical Trial number is assigned by ClinicalTrials.gov when a record is registered there; it is never created by a sponsor's software. Using the NCT prefix for internal keys invites confusion when the real number arrives, and an 8-digit time-derived value can coincide with a genuine registry number. Treat the returned id as a temporary internal key, replace it with your own protocol identifier (for example, the sponsor's study code, which later becomes the SDTM STUDYID), and store registry numbers such as NCT, EU CT or a national registry number in separate fields.

The method performs no input validation. Because TypeScript types vanish at run time, a JavaScript caller can pass phase: "V", a negative sample size or an empty title and receive a well-formed object. If the registration module in your system accepts input from users or from another service, validate the phase against TrialPhase, require a positive integer sample size and raise B001 or B003 yourself.

enrollPatient

enrollPatient creates a PatientEnrollment with status enrolled, today's date as enrollmentDate and the consentDate you supply. The subject identifier is the trial ID, a hyphen and a random three-digit number between 000 and 999. There is no check against identifiers already issued, so collisions follow the birthday problem. The probabilities below are exact calculations for a single trial, not estimates.

Subjects enrolledProbability that at least two share an ID
30≈ 36 %
50≈ 71 %
100≈ 99.4 %
1,001 or morecertain

Subject identifiers are the spine of every downstream dataset — the SDTM USUBJID is built from them — so this is not a cosmetic issue. Generate them deterministically instead: site number plus a site-level sequence (for example SITE01-0001), issued from a single authority with a uniqueness constraint in the database.

The second gap is consent. The error enumeration contains MISSING_CONSENT (B006), and spec §8.4 says that data collection cannot proceed without informed consent, yet enrollPatient accepts an empty string as consentDate and nothing in the package ever raises B006. The method also does not randomize: randomization is an optional field in the type, but there is no allocation function. Chapter 7 shows a consent guard that throws B006 before any write; until you have one, do not let enrollPatient be called on its own.

5.3 collectPatientData

Request and response

collectPatientData implements the second interface of spec §8.2. The request carries trialId, patientId, a free-text visit label and up to four payloads: measurements (a map from name to number), laboratoryResults (the nested LabResults object), vitalSigns and adverseEvents. The response has success, dataId (present only on success), validationErrors, warnings and timestamp. The split between errors and warnings is sound design: errors block the record, warnings are shown to the site or the data manager and become queries. The problem is which checks land in which list.

What is actually checked

Only one condition produces a validation error: a value in measurements that is not a number (a string such as "7.1" or NaN). Every other check produces a warning, so success stays true and a dataId is issued even for impossible values. The table compares each check with the validity limits in spec §8.3. Those limits are plausibility bounds for data entry (Chapter 7 treats the distinction in detail); the VITAL_SIGNS_RANGES and LAB_RANGES constants are clinical reference ranges, used for flagging. The code mixes the two.

FieldSDK checkSpec §8.3 validity limitConsequence
measurements.*must be a number (error)—No range check at all, so measurements.hba1c: 25 is accepted silently
Systolic BPwarn if < 90 or > 25060–250 mmHgLower bound is the normal-range minimum; 141–250 passes silently; 0 is skipped (falsy test)
Diastolic BPnone40–150 mmHg300 mmHg accepted without a warning
Heart ratewarn if < 30 or > 20030–200 bpmMatches the spec, but as a warning, not an error
Temperature, respiratory rate, SpO₂none35–42 °C; 6–60 breaths/min; —45 °C accepted without a warning
chemistry.hba1cwarn if < 3 or > 204.0–15.0 %Different bounds from the spec and the CLI; field not declared in the type
chemistry.glucosewarn if < 20 or > 60020–600 mg/dLMatches, as a warning
chemistry.alt, astwarn if > 200, labelled "Grade 2+"5–2000 U/LCTCAE grades depend on multiples of the site's upper limit of normal, so a fixed 200 U/L cannot be a grade
Creatinine, haematology, lipidsnonecreatinine 0.3–15.0 mg/dLNot checked
adverseEventsignored—No seriousness evaluation for AEs sent this way (Chapter 4)

Run with a handful of inputs, the method behaves as follows. The output is copied from an actual run; identifiers and timestamps are abbreviated.

vitalSigns { systolicBP: 200 }          -> success true,  warnings []
vitalSigns { systolicBP: 85 }           -> success true,  ["Systolic BP (85) is out of normal range"]
vitalSigns { systolicBP: 260 }          -> success true,  ["Systolic BP (260) is critically high"]
vitalSigns { systolicBP: 0 }            -> success true,  warnings []
vitalSigns { heartRate: 25, diastolicBP: 300, temperature: 45 }
                                        -> success true,  ["Heart rate (25) is out of valid range"]
chemistry  { hba1c: 2.5, glucose: 700, alt: 250 }
                                        -> success true,  3 warnings
measurements { hba1c: "7.1" }           -> success false, ["Invalid measurement value for hba1c"]

Two further facts complete the picture. The dataId is a random string; the data are not stored, so calling the method twice with the same visit produces two identifiers and no duplicate detection. And the spec's §8.2.2 result type declares dataId as mandatory, while the SDK omits it when validation fails — the SDK's optional field is the better design, but a client written against the spec text should expect it to be missing.

Using it safely

Treat collectPatientData as a first-pass flagging function, not as an edit-check engine. In your EDC layer, run the §8.3 validity limits as your own edit checks that raise DATA_OUT_OF_RANGE (B007) for every value outside them, keep those findings apart from the SDK's normal-range warnings, which are clinical flags, and add the checks the SDK omits. Chapter 7 explains why an out-of-limit value should be stored with an open query for source confirmation rather than rejected outright. The full edit-check and query model, including cross-form checks, is the subject of Chapter 7. Also normalise the input shape. The patient JSON in spec §7.2 puts HbA1c, sbp and dbp in measurements, whereas the SDK range-checks HbA1c only in laboratoryResults.chemistry and blood pressure only in vitalSigns. Data sent in the spec's own shape therefore receives no range check at all.

5.4 recordAdverseEvent

Chapter 4 examined this method against the regulations in detail; here we look at it only as an API. It takes an AdverseEvent without aeId, generates an identifier of the form AE-<time>-<random> and returns the event with two additional fields. reportingRequired is simply the seriousness.serious flag. reportingDeadline, present only when that flag is true, is startDate plus 7 days if death or lifeThreatening is set, and plus 15 days otherwise.

startDate "2026-03-02", serious + death            -> true,  2026-03-09
startDate "2026-03-02", serious + hospitalization  -> true,  2026-03-17
startDate "2026-03-02", hospitalization only
                        (serious: false)           -> false, undefined

The dates in the example are illustrative. The three lines show the three properties to keep in mind when calling the method. The deadline counts from onset rather than from the sponsor's awareness of the event, which is when regulatory clocks start. The decision ignores causality and expectedness, so it answers "is this an SAE?" rather than "is this an expedited report?". And the summary flag is trusted over the criteria: a record whose criteria say hospitalization but whose serious flag is false is not reported at all. Derive serious from the six criteria before calling, pass the awareness date to your own deadline function (Chapter 4 gives a corrected clock), and use the method mainly for its identifier and shape.

A typing detail matters for API users. The class method's return type includes the two extra fields; the standalone recordAdverseEvent() function declares a plain AdverseEvent return. At run time the fields are present, but TypeScript code calling the standalone function cannot read them without a cast. Call the method on an SDK instance instead.

5.5 Statistics APIs

Chapter 2 derived every formula used here and compared the sample-size and power functions with hand calculations. This section adds the details that matter when you call the functions from code: which parameters are honoured, what the result fields mean, and where the implementation approximates.

calculateSampleSize

The method chooses its branch by the presence of parameters, not by an explicit endpoint type. If standardDeviation is given, it uses the continuous formula of spec §4.1; otherwise, if both controlRate and treatmentRate are given, it uses the binary branch; otherwise it throws B005. It validates only that alpha and power lie strictly between 0 and 1. The result contains sampleSizePerGroup (rounded up), totalSampleSize (twice that), adjustedSampleSize (total divided by one minus the dropout rate, rounded up; default dropout 0.20), the echoed power and effectSize, and a method string. Because the per-group figure is rounded before doubling, the continuous branch reproduces the corrected values of Chapter 2: 91 per group for Appendix A.1 and 142 per group for the §4.1 example.

The critical values, however, are hard-coded. alpha is used only in the range check: a two-tailed test always uses Z = 1.96 and a one-tailed test Z = 1.645, the α = 0.05 values. Power is mapped to Z = 1.28 if it is at least 0.90 and to Z = 0.84 otherwise. So a request for 95 % power with σ = 1.5 and Δ = 0.5 returns 189 per group, the 90 % answer; the correct figure with Z = 1.645 is 234. A request for 85 % power returns the 80 % answer. The binary branch has the formula defect described in section 2.3 (the two terms are squared separately) and ignores effectSize. The rule is the same as in Chapter 2: use the method only for continuous endpoints at α = 0.05 two-sided and 80 % or 90 % power, and for anything else use a validated statistics package. Note also that the SDK's rounded Z values can differ by one subject from a calculation with more decimal places; document which convention your protocol uses.

calculatePower

sampleSize here means subjects per group. With a standardDeviation, the method computes power = Φ(Δ / (σ√(2/n)) − Z), where Z is again fixed at 1.96 or 1.645, so the alpha field is ignored apart from being echoed back. The normal CDF is a well-known polynomial approximation (Abramowitz and Stegun 26.2.17) that is accurate to about seven decimal places, so within those conditions the output is reliable: n = 91 per group, σ = 1.2, Δ = 0.5 gives 0.802. Without a standard deviation, the method does not refuse. It returns min(0.99, 0.5 + 0.4 × (n/100) × Δ), a heuristic with no statistical basis, which reports 0.70 for n = 100 and Δ = 0.5 whatever the variability. Always pass the standard deviation.

performHypothesisTest and calculateDescriptiveStats

performHypothesisTest(group1, group2, alpha) runs a pooled-variance two-sample t-test and returns the statistic, degrees of freedom, p-value, a confidence interval, the difference in means (effectEstimate), its standard error and a significant flag. The statistic and standard error are computed correctly. The p-value and interval are not, whenever the sample is small. For df > 30 the t-distribution is replaced by the normal, which is a reasonable approximation. For df ≤ 30 the code uses 1 − 0.5·(df/(df + t²))^(df/2), which is not the t-distribution function. At the exact 5 % critical value (t = 2.228, df = 10) it gives a two-sided p of about 0.13 instead of 0.05. The interval always uses 1.96 in place of the t quantile, so it is too narrow for small samples, and it is a 95 % interval even if you pass a different alpha.

A small example makes the effect concrete. Two groups of six HbA1c values (illustrative data):

const g1 = [7.1, 6.8, 7.4, 6.9, 7.6, 7.0];
const g2 = [7.9, 8.1, 7.5, 8.4, 7.8, 8.0];
sdk.performHypothesisTest(g1, g2);

// SDK output                              exact (t-distribution, df = 10)
// testStatistic       -4.643              -4.643
// standardError        0.1759              0.1759
// pValue               0.0032              0.00092
// confidenceInterval  [-1.161, -0.472]    [-1.209, -0.425]

Here the conclusion happens to be the same, but the p-value is off by a factor of about 3.5 and the interval is about 12 % too narrow; near the significance boundary the significant flag can be wrong in either direction. There is no Welch (unequal-variance) option and no paired test.

calculateDescriptiveStats returns n, mean, SD (with the n − 1 denominator), standard error, median, minimum, maximum, quartiles and a 95 % interval for the mean. The quartiles are taken as sorted[floor(0.25·n)] and sorted[floor(0.75·n)] without interpolation: for the data 1, 2, 3, 4 the SDK reports Q1 = 2 and Q3 = 4, whereas the common interpolating definition gives 1.75 and 3.25. Statistical software offers several quartile definitions, and the one used must be stated in the statistical analysis plan; this one is not among the usual choices. For a single value the SD, SE and interval are NaN (serialised as null in JSON) with no error. The interval again uses 1.96.

calculateStatistics

This standalone function corresponds to the §8.2.3 "calculate statistics" interface. It dispatches on which fields are present: sampleSize without power calculates power; power without sampleSize calculates sample size; anything else throws B005. Its parameter type has no standardDeviation and no event rates, and it does not forward them even if a JavaScript caller adds them. As a result the power branch always uses the heuristic (n = 300, Δ = 0.5 returns 0.99) and the sample-size branch always throws B005. The spec's StatisticalResult type, with pValue and confidenceInterval, is not what the function returns: it returns the union PowerResult | SampleSizeResult. Do not use this function; call calculatePower or calculateSampleSize directly.

CallHonoured inputsSafe to use when
calculateSampleSizestandardDeviation, effectSize, testType, dropoutRate; power only as ≥ 0.90 or notcontinuous endpoint, α = 0.05, power 0.80 or 0.90
calculatePowersampleSize (per group), effectSize, standardDeviation, testTypeσ supplied, α = 0.05
performHypothesisTestboth arrays, alpha for the flag onlydf > 30 (large samples), equal variances plausible
calculateDescriptiveStatsthe arrayn, mean, SD, median, min, max; not quartiles
calculateStatistics—never

5.6 generateSubmission

The name suggests more than the method does. generateSubmission creates a RegulatorySubmission record with a generated id, the inputs echoed back, submissionDate set to now, status: 'draft' and an empty comments array. It does not read datasets, produce SDTM or ADaM files, write define.xml, or assemble an eCTD. The record is useful as the first row of a submission-tracking table whose status then follows the lifecycle draft → submitted → under-review → approved | rejected (Chapter 6). Setting submissionDate at draft creation is misleading; record the actual transmission date when the status changes to submitted.

EnumerationValuesNotes
RegulatoryAgencyFDA, EMA, PMDA, NMPA, MHRA, OTHERNational authorities not listed, such as Korea's MFDS, need OTHER plus a separate field
SubmissionFormatCDISC, eCTD, NeeS, SENDThese are not the same kind of thing: eCTD and NeeS are dossier formats, CDISC is a family of data standards and SEND is the CDISC standard for nonclinical data
typeIND, NDA, BLA, CTA, MAA, OTHERIND, NDA and BLA are US application types; CTA and MAA are the European terms for clinical-trial and marketing-authorisation applications
statusdraft, submitted, under-review, approved, rejectedNo state for requests for information, clinical holds or withdrawal

The standalone generateSubmission() accepts only trialId, agency and format, and fills in type: 'OTHER' and datasets: []. Use the class method so that the application type and the list of datasets are recorded from the start. The content of each regulatory package — CTD modules, IND forms, EU CTA parts — is covered in Chapter 6.

5.7 Errors

Failures are reported with ClinicalTrialError, a subclass of Error with a code of type ClinicalTrialErrorCode and an optional details record. Spec §8.4 lists six codes; the SDK enumeration adds four more. Only one is ever thrown by the package itself.

CodeEnum nameIn spec §8.4Raised by the SDKWhere your application should raise it
B001INVALID_TRIAL_IDyesnoany call whose trialId is not a registered trial
B002PATIENT_NOT_ENROLLEDyesnodata or AE for a subject not in enrolled/randomized/active status
B003VALIDATION_FAILUREyesno (reported as success: false)structural or type errors in a data payload
B004SAE_NOT_REPORTEDyesnoa scheduled job that finds an expedited report past its deadline
B005STATISTICAL_VIOLATIONyesyes: invalid α or power, missing SD/rates, empty dataalso for unsupported α or power (section 5.9)
B006MISSING_CONSENTyesnoenrolment or data collection without a valid consent record
B007DATA_OUT_OF_RANGEno (SDK only)novalues outside the §8.3 validity limits
B008PROTOCOL_DEVIATIONno (SDK only)novisits outside window, dosing errors (Chapter 6)
B009REGULATORY_NONCOMPLIANCEno (SDK only)noe.g. a submission record missing required datasets
B010EDC_SYSTEM_ERRORno (SDK only)noinfrastructure failures in your own layer

Because the codes are string enum values, they serialise cleanly into logs and HTTP error bodies, and they are stable identifiers to build your own mapping on. Two practical recommendations: check err instanceof ClinicalTrialError before reading err.code, since other exceptions can reach the same handler; and put the offending field and value in details so that data managers can act on the error without reading a stack trace. When B007–B010 appear in messages exchanged with partners, note that they are extensions of the SDK and not part of the v1.0 specification text.

5.8 The CLI and its limits

wia-bio-010 is a Bash script with six working commands plus version and help. It does not call the TypeScript package, does not read or write files, and keeps no state between runs. Its constants are hard-coded at the top of the script. It is best understood as a demonstration printer: it formats plausible output for a handful of scenarios. The table summarises each command as observed when run.

Command and flagsWhat it printsWhat to know
register-trial --title --phase --sample-sizea new NCT… ID from the clock, status PLANNED, a fixed timeline (12-month enrolment, 52 weeks, 4-week follow-up, completion in 18 months)phase is not validated (--phase V is accepted); timeline is not derived from the inputs
record-data --trial-id --patient-id --hba1cHbA1c against 4.0–15.0 % and a 7.0 % target, then "Data collection completed" and a data IDout-of-range and non-numeric values print an error line but still "complete", and the exit code is 0
report-ae --trial-id --patient-id --severityalways the same hypoglycaemia event with fixed coding, causality and outcomeseriousness inferred from intensity; always a 15-day deadline from today (Chapter 4)
calc-sample-size --alpha --power --effect-size --sdZ values, per-group, total and dropout-adjusted sizes, a feasibility labelarithmetic and rounding errors; --alpha is ignored
calc-power --sample-size --alpha --effect-sizea power percentage and an adequacy labeluses the 0.5 + 0.4·(n/100)·Δ heuristic; no SD input exists
generate-submission --trial-id --agency --formata submission ID, "New Drug Application", dataset lists and "SDTM v1.7 compliant" / "ADaM v1.1 compliant" linescreates nothing and checks nothing; the compliance lines are fixed text
version, helpversion 1.0.0, usagewia-bio-010 --version (as written in the README) is an unknown command

The sample-size command illustrates why the CLI's numbers must not be quoted. With the defaults (α 0.05, 80 % power, Δ 0.5, σ 1.5) it prints:

Sample Size per Group: 141.5 patients
Total Sample Size: 283.0 patients
Adjusted for 20% Dropout: 353.5 patients
[: 283.0: integer expression expected      (three times, on stderr)
Very large trial (mega-trial)

The script means to round up by adding 0.5 inside bc, but the addition inherits the scale of its operands, so the result is 141.5 instead of an integer (the correct answer is 142). The decimal totals then break Bash's integer comparisons, every feasibility test fails, and the script falls through to "mega-trial" — for 283 subjects, and equally for 11. Passing --alpha 0.01 changes the α line in the output but not the Z value of 1.96 used in the calculation. The calc-power command with 100 per group and Δ = 0.5 reports "70.0000 %"; the normal-approximation power for σ = 1.5 is 65 % (Chapter 2), and the CLI cannot take σ into account at all.

Two properties matter if anyone scripts around the CLI. Commands exit with status 0 even when they print an error line, so a pipeline cannot detect failures. And unknown flags are silently skipped, so a typo such as --effectsize quietly uses the default. Keep the CLI for demonstrations and training; for any number that goes into a protocol, a report or a database, use a validated tool.

5.9 Putting it together: a corrected quick start and guards

The README examples

README.md contains two TypeScript examples that do not work as written. The "Quick Start" calls calculateSampleSize with effectSize: 0.3 but without a standard deviation or event rates, so it throws B005 before printing anything. The "Components" example calls calculateStatistics with a sample size and reads power.statisticalPower and power.pValue; neither property exists, so TypeScript rejects the code and plain JavaScript prints undefined undefined, while the actual power field holds the heuristic 0.99. The version below uses only calls that behave as documented within the limits of this chapter. It compiles under --strict against the patched package described in section 5.1 and produces the output shown.

import { ClinicalTrialSDK, ClinicalTrialError } from '@wia/bio-010';

const sdk = new ClinicalTrialSDK();

// 1. Sample size: continuous endpoint, standardDeviation is mandatory
const ss = sdk.calculateSampleSize({
  alpha: 0.05,            // only 0.05 is honoured (Z fixed at 1.96)
  power: 0.80,            // only 0.80 or 0.90 are honoured
  effectSize: 0.5,        // difference in HbA1c, % points
  standardDeviation: 1.2, // % points
  testType: 'two-tailed',
  dropoutRate: 0.20,
});
console.log('per group:', ss.sampleSizePerGroup,
            'total:', ss.totalSampleSize,
            'with dropout:', ss.adjustedSampleSize);

// 2. Register the trial with the dropout-adjusted size
const trial = sdk.registerTrial({
  title: 'Phase III HbA1c study (example)',
  phase: 'III',
  indication: 'Type 2 diabetes mellitus',
  primaryEndpoint: 'HbA1c',
  sampleSize: ss.adjustedSampleSize ?? ss.totalSampleSize,
  duration: 24,
});
console.log('status:', trial.status, 'timepoint:', trial.primaryEndpoint.timepoint);

// 3. Power check: pass standardDeviation, otherwise a heuristic is used
const pw = sdk.calculatePower({
  sampleSize: ss.sampleSizePerGroup, // per group
  alpha: 0.05,
  effectSize: 0.5,
  standardDeviation: 1.2,
  testType: 'two-tailed',
});
console.log('power:', pw.power.toFixed(3));

// 4. Visit data: vitals and labs are checked, free measurements must be numbers
const res = sdk.collectPatientData({
  trialId: trial.id,
  patientId: 'SITE01-0001',
  visit: 'Week 12',
  measurements: { hba1c: 6.8, fasting_glucose: 121 },
  vitalSigns: { systolicBP: 86, heartRate: 72 },
});
console.log('success:', res.success, 'warnings:', res.warnings);

// 5. Errors carry a B-code
try {
  sdk.calculateSampleSize({ alpha: 0.05, power: 0.8, effectSize: 0.3, testType: 'two-tailed' });
} catch (e) {
  if (e instanceof ClinicalTrialError) console.log('error:', e.code, '-', e.message);
}

Expected output:

per group: 91 total: 182 with dropout: 228
status: planned timepoint: Week 24
power: 0.802
success: true warnings: [ 'Systolic BP (86) is out of normal range' ]
error: B005 - Must provide either standardDeviation (continuous) or controlRate/treatmentRate (binary)

The first line reproduces Appendix A.1 of the spec, and the third confirms that the design has the intended 80 % power. The fourth line shows the flagging behaviour of section 5.3: a systolic pressure of 86 mmHg is plausible, below the normal range, and correctly raised as a warning rather than rejected. The last line is what the README's own quick start would print.

A guard layer

Rather than remembering every restriction at each call site, wrap the statistics methods once so that unsupported inputs fail loudly. The wrapper below rejects the inputs for which the v1.0 code is known to give wrong answers and otherwise delegates unchanged.

import {
  ClinicalTrialSDK, ClinicalTrialError, ClinicalTrialErrorCode,
  SampleSizeCalculation, SampleSizeResult, PowerCalculation, PowerResult,
} from '@wia/bio-010';

const sdk = new ClinicalTrialSDK();
const B005 = ClinicalTrialErrorCode.STATISTICAL_VIOLATION;

/** Accept only the inputs the v1.0 code actually computes correctly. */
export function guardedSampleSize(p: SampleSizeCalculation): SampleSizeResult {
  if (p.alpha !== 0.05) throw new ClinicalTrialError(B005, 'alpha must be 0.05 in v1.0');
  if (p.power !== 0.8 && p.power !== 0.9)
    throw new ClinicalTrialError(B005, 'power must be 0.80 or 0.90 in v1.0');
  if (p.standardDeviation === undefined)
    throw new ClinicalTrialError(B005, 'binary branch disabled: use a validated tool');
  return sdk.calculateSampleSize(p);
}

export function guardedPower(p: PowerCalculation): PowerResult {
  if (!p.standardDeviation || p.standardDeviation <= 0)
    throw new ClinicalTrialError(B005, 'standardDeviation is required');
  if (p.testType !== 'two-tailed' || p.alpha !== 0.05)
    throw new ClinicalTrialError(B005, 'only two-sided alpha = 0.05 is supported');
  return sdk.calculatePower(p);
}

// guardedSampleSize({ alpha: 0.05, power: 0.9, effectSize: 0.5,
//   standardDeviation: 1.5, testType: 'two-tailed' })   -> 189 per group
// the same with alpha: 0.01                              -> B005 alpha must be 0.05 in v1.0

The same pattern applies to the other calls: a registration wrapper that validates phase and sample size and substitutes your own study code for the generated ID; an enrolment wrapper that issues sequential subject IDs and refuses to proceed without consent (Chapter 7 gives the B006 guard); a data-collection wrapper that raises B007 queries for values outside the §8.3 limits alongside the SDK call; and an AE wrapper that derives serious from the criteria and computes deadlines from the awareness date. Keep the wrappers thin and cover them with the test vectors of Chapter 8, so that when a later SDK release fixes a defect you can remove the corresponding guard and see the tests stay green.

Known defects of the v1.0 SDK and CLI (API view)

Chapter checklist

Before you rely on the SDK, confirm that…Section
You build from source with the export blocks fixed and pin the exact commit you validated.5.1
Trial and subject identifiers come from your own sequence, not from the SDK.5.2
Consent is checked before enrolment and before every data write.5.2, Ch. 7
§8.3 validity limits are checked outside the SDK and raise B007 queries; SDK normal-range warnings are kept as clinical flags.5.3
Seriousness is derived from the criteria, and reporting clocks start at awareness.5.4, Ch. 4
Statistics calls go through guards; anything outside α 0.05 / power 0.80–0.90 / continuous endpoints uses a validated package.5.5, 5.9
No number from the CLI appears in a protocol, report or database.5.8
Your own code raises B001–B010 consistently and records details.5.7

With the interfaces understood — including where they fall short — the next chapter steps back from individual calls to the protocol as a whole: the order in which these operations happen over the life of a trial, the state transitions they drive, and the monitoring and regulatory steps that surround them.