Chapter 4

Chapter 4: The SDK and CLI as Record APIs

The reference SDK and command-line tool of WIA-BIO-014, read strictly as services that produce and check records — their request and response contracts, their error codes, and the provenance an institution must add around every call.

Chapters 2 and 3 described what a protocol record contains and how it is serialized and identified. This chapter describes the machinery that produces and checks those records: the reference TypeScript SDK published as the package @wia/bio-014 with the class CRISPRProtocolSDK, and the reference shell tool wia-bio-014. The framing is deliberate and it does not change for a single section. Every entry point in this surface is treated as a record-producing or a record-checking service, and of each one we ask only three questions: what record does it consume, what record does it emit, and what provenance must travel alongside that record so that the record is still defensible a year later, in front of a reviewer who was not present when the call was made.

How any number inside those records is computed is a scientific question. As stated in Chapter 1 §1.3, this book does not reproduce design or scoring methodology; where a response field carries such a number we document the field — name, type, cardinality, and required provenance — and refer the interpretation of its value to the investigator and to the institutional biosafety and ethics committees.

4.1 The SDK surface: one class, five functions, no memory

The reference SDK exposes a single class and five module-level functions. The class CRISPRProtocolSDK carries getVersion() plus five working methods: designGuideRNA(), predictOffTargets(), calculateEditingEfficiency(), validateProtocol(), and simulateEditing(). Beside the class, the module exports five standalone functions — designGuideRNA, predictOffTargets, calculateEditingEfficiency, validateCRISPRProtocol, and simulateCRISPREditing — and each of them constructs a fresh SDK instance and delegates to the corresponding method. Two naming asymmetries are worth noting before an institution writes its own wrapper: the class method is validateProtocol while the standalone function is validateCRISPRProtocol, and the class method is simulateEditing while the standalone is simulateCRISPREditing. Code that is written against one name and documentation that is written against the other will diverge silently.

The instance holds a version string and an initialization flag and nothing else. There is no session, no connection, no cache, and no state that survives a call. For a record-keeping system that single property is both the good news and the bad news: the surface is trivially safe to call concurrently and has no hidden ordering effects, but the SDK also does not know who is calling, which request this is, or which protocol version the caller believes it is working on, and it does not write anything anywhere. Every one of those facts is a field the institution's record store must supply.

Table 4-1. The reference SDK surface read as record services
Entry point Input record Output record Raises What the caller must store beyond the output
getVersion()—version string—Nothing; but the returned string belongs in every envelope below
designGuideRNA()GuideRNARequestGuideRNAC004The full request, tool version, actor, timestamp source
predictOffTargets()OffTargetRequestOffTargetSite[]C004The search parameters, the implementation identity, row count, truncation state
calculateEditingEfficiency()EfficiencyRequestEfficiencyResponseC007Both denominators, the threshold configuration behind the labels
validateProtocol()ProtocolValidationValidationResult—The three request flags, a digest of the protocol version validated, tool version
simulateEditing()CRISPRProtocolEditingSimulation—An idempotency key, an explicit "prediction" label, tool version

The right-hand column is the substance of this chapter, and it motivates a single structure that recurs in every section: the evidence envelope. An envelope is the record an institution actually stores; the SDK response is only its payload. At minimum an envelope carries the name and version of the tool that produced the payload, a canonical snapshot or digest of the input, the identity of the actor on whose authority the call was made, the source of the timestamp, and any request flags that changed the meaning of the payload. Nothing in the SDK produces an envelope. If the institution does not build one, the stored response is a number without a history, and a number without a history cannot be defended.

A fact about predictOffTargets() that governs how its output may be stored. In the reference implementation this method is a demonstration stub. Its own source comment states that a real implementation would search a genome database and that, for demonstration, mock sites are generated instead; the rows are filled by helpers named generateMockCoordinates, generateMockAnnotation, and generateOffTargetSequence, and mismatch counts and chromatin states are drawn from Math.random(). Two consequences follow, and neither is a matter of opinion. First, the output is not reproducible: two calls with byte-identical input return different rows, so the call can never be replayed to reconstruct a past record. Second, the rows must never be stored in any field a later reader could mistake for a measurement or for the result of a real genome-wide search. An institution adopting the interface must place a real predictor behind the same contract and record, per row set, which implementation produced it.

4.2 Request and response contracts

Three of the five calls are request-in, response-out contracts over records that exist nowhere else in the standard. They are described here field by field, as records; the two calls that operate on a whole CRISPRProtocol are treated separately in §4.3 and §4.4.

4.2.1 GuideRNARequest → GuideRNA

The request carries a target sequence string, a PAM type drawn from the PAMType vocabulary, an organism drawn from the Organism vocabulary, and five optional locator fields: chromosome, position, strand, gene name, and a custom genome reference. The response carries a sequence and a PAM string, a combined full-target string, four numbers — an on-target score, a GC-content percentage, a predicted-efficiency percentage, and a specificity score — a list of warning strings, and two optional structures: genomic coordinates and a list of predicted off-target sites.

The record rule is simple to state and is violated constantly in practice. The response contains four numbers whose scale and meaning are defined only by the implementation that produced them, so none of them means anything stored alone: a registry row reading "on-target score 0.72" and nothing else is not a fact about a guide but a fact about a program run that nobody recorded. Store request and response together, in one envelope, with the tool version from getVersion().

Two specific traps sit in this contract. The first is warnings: string[]. These are sentences intended for a human reader, not codes; they cannot be counted, filtered, or compared across a portfolio, and their wording may change between tool versions without any version signal in the payload. The institution should map each warning to its own coded vocabulary at the moment of intake and keep the original string verbatim beside the code, so that a future reader can see both what the tool said and how the institution classified it. The second trap is coordinates.assembly. In the reference implementation this value is not taken from the caller; it is derived from the organism field by an internal helper. The stored record therefore contains an assembly label the submitter never stated. A conformant record keeps the submitter's own assembly declaration as the authoritative field and treats any derived label as a tool artifact — the referential check that the two agree belongs to the validation layer described in Chapter 3 §3.4.

4.2.2 OffTargetRequest → OffTargetSite[]

The request carries a guide sequence string, a genome assembly string, a maximum-mismatch integer, and four optional booleans that widen or narrow the search: include DNA bulges, include RNA bulges, include PAM variants, and filter by chromatin accessibility. Each returned row carries genomic coordinates, a sequence string, a mismatch count, an array of mismatch positions, optional bulge counts, two scores named after published scoring methods, an annotation structure, an optional chromatin state, and a predicted-activity percentage. The annotation structure itself carries a region value from a six-term controlled vocabulary — exon, intron, promoter, intergenic, UTR, enhancer — plus optional gene name, gene identifier, transcript identifier, and a free-text consequence field.

Because the row set is a function of the search parameters, a set of rows stored without its parameters is uninterpretable: the same guide searched with a different maximum-mismatch value legitimately yields a different number of rows, and a reader who sees only the rows cannot tell a narrow search from a clean genome. The rule that follows is absolute in a record system. Row sets produced under different parameters are never merged into one table, and a row set is never stored without the request that produced it. The two score fields are named after published methods; the record must name the implementation and build that produced them, and this book neither reproduces nor endorses any scoring formula.

The contract also omits three things an automated pipeline needs, and the envelope must supply all three: the response is a bare array with no total count, so truncation is invisible; the genome assembly actually searched is not echoed, so a typo cannot be caught from the response; and there is no indication of which optional booleans the implementation honoured.

4.2.3 EfficiencyRequest → EfficiencyResponse

This is the one call in the surface that is pure arithmetic over values the caller supplies, and it is therefore the one whose record discipline can be stated exactly. The request carries an edited-read count, a total-read count, an edit type from the EditType vocabulary, and an optional sequencing-method label. The response carries an efficiency percentage, a two-element confidence interval, a three-valued quality label, a sample-size adequacy boolean, a recommendation string, and an optional statistics object holding a standard error.

The reference implementation computes the percentage as edited reads divided by total reads, times one hundred, and builds the interval as a normal-approximation interval around that proportion, using the standard error of a proportion and the two-sided 95 % standard normal quantile of 1.96. Both of those are established statistical facts rather than claims of this standard, and two further established properties matter for anyone storing the output. The normal approximation is known to behave poorly when the total count is small or when the proportion lies close to zero or one; and the reference implementation clamps the interval endpoints into the range 0 to 100, which conceals exactly the cases where the approximation has failed. A stored interval must therefore always be accompanied by the counts, so that a reader can recompute it — or recompute a better one — rather than trusting a clamped pair of numbers.

This is the API-layer restatement of the rule introduced in Chapter 2 §2.5: a computed percentage is never stored without its denominators. The function itself enforces the inverse of that rule at its input boundary, refusing an edited count greater than the total and refusing a total of zero, both with error code C007. The asymmetry is instructive. The tool will not accept incoherent counts, yet it will happily hand back a percentage that the caller is free to store with the counts discarded — at which point no validator anywhere in the system can ever check it again.

The quality label and the adequacy boolean are produced by comparing the total read count against integer thresholds hard-coded in the reference implementation. Chapter 3 §3.5 covers those constants and states plainly that they are unsourced, configurable software defaults rather than scientific recommendations. At the API layer only one rule is needed: store the label together with the threshold configuration that produced it, or the label becomes unauditable the first time a default changes. The recommendation string deserves stronger treatment still. It is assembled by concatenating a sample-size sentence with a comparison against a hard-coded "typical" efficiency for the edit type — so it is both unparseable and dependent on an unsourced constant. An institution should either omit it from stored records or store it as opaque vendor text, explicitly attributed to the tool and its version, and never let a downstream report present it as a finding of the institution.

Table 4-2. Divergences between the specification's §8.2 interfaces and the SDK types
Element Specification §8.2 SDK types.ts Record consequence
Design response type nameGuideRNAResponseGuideRNADocumentation and code refer to the same record by two names
PAM vocabularyThree valuesPAMType with additional values including a "none" caseA value valid in the SDK is rejected by a spec-literal validator
Off-target locationFlat chromosome + positioncoordinates object with strand and assemblyThe spec shape cannot carry strand or assembly; lossy in both directions
Mismatch representationOne array field named mismatches holding positionsmismatches count plus mismatchPositions arraySame field name, different type and meaning — a silent corruption risk
Off-target annotationA single stringOffTargetAnnotation object with a region vocabularyOnly the SDK shape is queryable; the string form must be parsed to migrate
Efficiency responseFour fieldsAdds adequacy boolean and statistics objectA spec-literal consumer drops fields it does not know (Chapter 3 §3.6)
Off-target list in design responseRequiredOptionalAbsence is ambiguous: not requested, or requested and empty

The practical conclusion is that an implementer must declare which of the two shapes their interface follows and must publish the adapter between them. This book recommends the SDK shape as the record shape, because it is the one that can carry strand, assembly, and a queryable annotation, with the specification shape supported through a documented, lossless-in-one-direction adapter. The field-name mapping between the JSON document and the SDK types belongs to Chapter 3 §3.1 and is not repeated here.

4.3 validateCRISPRProtocol: turning a validation run into evidence

The validation call takes a ProtocolValidation record — a protocol plus three optional booleans, all of which default to true in the reference implementation: check ethics, check regulatory, and perform off-target analysis. It returns a ValidationResult carrying a validity boolean, an array of blocking error strings, an array of non-blocking warning strings, a safety sub-object with an off-target risk level, an overall risk level and two compliance booleans, a quality sub-object with three numeric scores, an array of recommendation strings, and a timestamp.

Three properties of that record decide whether it can serve as evidence, and all three are visible in the reference implementation.

First, the validity boolean is defined as "the error array is empty". It is therefore a summary of one run under one configuration, not a property of the protocol. Two runs of the same validator over the same protocol with different flags can legitimately disagree, and the record as returned gives a reader no way to tell which configuration produced which answer.

Second, the two compliance booleans are satisfied either by the protocol or by the caller having switched the check off. Ethics compliance is computed as "an ethics approval is recorded, or the ethics check was disabled"; regulatory compliance as "the regulatory array is non-empty, or the regulatory check was disabled". A caller who disables the ethics check therefore receives a result that reads as ethics-compliant for a protocol carrying no approval whatsoever — and the flags are not echoed anywhere in the returned record. This yields the hardest rule in this chapter: a stored ValidationResult without its request flags is not evidence. The flags belong in the envelope, and a record store should refuse to accept a validation result that does not carry them. Note also that regulatory compliance here is satisfied by the presence of array entries, not by their content; as Chapter 5 §5.2 sets out, an attestation flag can never stand in for a referenced approval record.

Third, the timestamp is the machine clock of whoever ran the validator, and the record carries no tool version and no digest of the protocol that was validated. Stored alone, it cannot answer the two questions a reviewer always asks: which version of the record was validated, and by which build of the validator. Chapter 5 §5.4 covers clock synchronization as the precondition for orderable audit events; at the API layer the requirement is narrower and non-negotiable — the envelope records where the timestamp came from.

One further limitation shapes how the result should be indexed. The error and warning arrays are plain strings: no codes, no field paths. A record store that wants to ask "how many protocols were blocked for a missing approval this quarter" cannot answer it from these arrays without string matching, which breaks at the next wording change. The recommended intake treatment is to map each string to an institution-local code plus a field pointer — JSON Pointer, defined by RFC 6901, is the appropriate notation for the path — while retaining the original text verbatim. Which validation layer each individual check belongs to is Chapter 3 §3.4's subject; what matters here is that the reference validator performs structural checks and a handful of policy checks, and does not verify that any referenced approval document exists.

4.3.1 An evidence envelope in code

The following excerpt shows the shape of a wrapper that converts a validation call into a storable record. It adds nothing scientific; it adds only the provenance the SDK does not carry. The Result type it returns is discussed in §4.5.

import { createHash } from 'node:crypto';
import {
  CRISPRProtocolSDK,
  validateCRISPRProtocol,
  type CRISPRProtocol,
  type ProtocolValidation,
  type ValidationResult,
} from '@wia/bio-014';

interface EvidenceEnvelope<T> {
  envelopeVersion: 'wia-bio-014-env/1';
  tool: { name: string; version: string };
  actor: { subject: string; authority: string };
  input: { protocolId: string; protocolVersion: string; digest: string };
  flags: Record<string, boolean>;
  clock: { source: 'ntp-sync-host'; recordedAt: string };
  payload: T;
}

function canonicalDigest(value: unknown): string {
  // Chapter 3 §3.7 requires RFC 8785 canonical JSON before hashing;
  // JSON.stringify is shown for brevity and is NOT key-order stable.
  return createHash('sha256')
    .update(JSON.stringify(value))
    .digest('hex');
}

export function validateAsEvidence(
  protocol: CRISPRProtocol,
  actor: { subject: string; authority: string },
): EvidenceEnvelope<ValidationResult> {
  const sdk = new CRISPRProtocolSDK();

  const request: ProtocolValidation = {
    protocol,
    checkEthics: true,
    checkRegulatory: true,
    performOffTargetAnalysis: true,
  };

  const payload = validateCRISPRProtocol(request);

  return {
    envelopeVersion: 'wia-bio-014-env/1',
    tool: { name: '@wia/bio-014', version: sdk.getVersion() },
    actor,
    input: {
      protocolId: protocol.id,
      protocolVersion: protocol.version,
      digest: canonicalDigest(protocol),
    },
    // The three flags are echoed because the payload does not carry them.
    flags: {
      checkEthics: request.checkEthics === true,
      checkRegulatory: request.checkRegulatory === true,
      performOffTargetAnalysis: request.performOffTargetAnalysis === true,
    },
    clock: { source: 'ntp-sync-host', recordedAt: new Date().toISOString() },
    payload,
  };
}

Serialized, the stored record looks like the document below. Every value shown is a placeholder, marked as such; nothing here is a recommended configuration.

{
  "envelopeVersion": "wia-bio-014-env/1",
  "tool": { "name": "@wia/bio-014", "version": "<sdk-version>" },
  "actor": { "subject": "<institutional-user-id>", "authority": "role:submitter" },
  "input": {
    "protocolId": "<protocol-id-placeholder>",
    "protocolVersion": "<version-placeholder>",
    "digest": "sha256:<hex-digest-placeholder>"
  },
  "flags": {
    "checkEthics": true,
    "checkRegulatory": true,
    "performOffTargetAnalysis": true
  },
  "clock": { "source": "ntp-sync-host", "recordedAt": "<ISO-8601 timestamp>" },
  "payload": {
    "isValid": false,
    "errors": ["<verbatim validator message>"],
    "warnings": ["<verbatim validator message>"],
    "safety": {
      "offTargetRisk": "<low|medium|high>",
      "overallRisk": "<low|medium|high>",
      "ethicsCompliant": false,
      "regulatoryCompliant": false
    },
    "quality": {
      "gRNAQuality": 0,
      "specificityScore": 0,
      "protocolCompleteness": 0
    },
    "recommendations": ["<verbatim validator message>"],
    "validated": "<ISO-8601 timestamp>"
  },
  "localCodes": [
    { "code": "<institution-local-code>", "pointer": "/safety/ethicsApproval" }
  ]
}

The localCodes array is the institution's own addition: the coded, queryable form of the free-text arrays, each entry pointing at the field it concerns. The payload is preserved byte-for-byte beside it, because the institution's classification and the tool's words are two different claims and a later reviewer is entitled to both. One numeric field is worth a caution: the completeness score in the quality object is computed in the reference implementation by awarding a fixed weight for the mere presence of each of five child structures. It measures whether objects exist, not whether they are adequately filled, and a record should never present it as a measure of quality.

4.4 simulateCRISPREditing: planning artifacts kept apart from measurements

The simulation call takes a whole protocol and returns an EditingSimulation: an identifier, the input protocol embedded verbatim, a predicted-outcomes object, an off-target list copied from the guide record, a success probability between zero and one, a five-part timeline of strings, and an optional cost estimate with a currency. Read as a record, this object is a planning artifact, and the single most important rule about it is that it must never be stored where a reader could take it for a measurement.

The reference implementation makes that rule easy to justify. Its timeline is five hard-coded strings, identical for every input, and its predicted indel distribution is three hard-coded integers, likewise identical for every input. The object is therefore a template with a few computed fields threaded through it, not a prediction in any meaningful sense. Anything carried from it into a record must be labelled as the output of a named tool version, and the timeline strings in particular must never be transcribed into a project plan as schedule commitments — they are illustrative placeholders inherited from the reference code.

The success probability is derived from values that are themselves derived from the unsourced constants discussed in Chapter 3 §3.5, and from a delivery-efficiency field that the implementation replaces with a default when the protocol omits it. Store it only with the tool and version attached, and never in the same field family as the measured outcome records of Chapter 2 §2.5. The rule generalizes: predicted values and measured values live in separate record types, joined for reporting by protocol identifier and version, and any report that shows them together states in the column heading which is which. The most common way institutions lose the ability to answer a reviewer is by writing a prediction into the field that later holds the measurement.

Three smaller properties have direct record consequences. The simulation identifier is composed of a millisecond timestamp and a short random suffix, so it is not derived from the input: re-running the identical input produces a new identifier and there is no way to detect that the two records describe the same run. An institution that stores simulations should therefore carry its own idempotency key beside the SDK identifier, computed as a digest of the canonical input plus the tool version — the same construction used in §4.3 and revisited in §4.7. The cost estimate is optional and the reference implementation never populates it, so absence must be recorded as "not provided" and never coerced to zero. And the whole input protocol is embedded in the returned object, which duplicates the protocol record — including any personal data that has leaked into description fields, a risk Chapter 2 §2.7 catalogues. Storing a reference and a digest instead of the embedded copy is the better default; where the embedded copy is kept, it inherits the access controls of the protocol itself, which Chapter 6 §6.3 specifies.

4.5 Error codes C001–C010 and the caller's obligations

Errors in the SDK are raised as a CRISPRProtocolError, a subclass of the platform error type carrying a code from the CRISPRErrorCode vocabulary, a message, and an optional details map. The vocabulary defines ten codes, C001 through C010. Three facts about this vocabulary need stating before the table, because each of them changes how an implementer should plan error handling.

The specification's error table in §8.4 lists only six codes, C001 through C006. The SDK enumeration adds four more: invalid parameters, genome not found, off-target analysis failed, and validation failed. An implementer reading only the specification will not handle the four additional codes, and one of them is among the only two codes the reference implementation actually raises.

Of the ten codes, only two are ever raised anywhere in the reference implementation: the invalid-sequence code, raised at two input checks, and the invalid-parameters code, raised at two further input checks. The remaining eight are declared but unreachable. This is not a defect to be papered over in documentation; it is a fact that shapes conformance. A claim about error handling must be made per code and backed by a test vector that actually provokes the code — Chapter 7 §7.2 sets out how — and an institution adopting the standard must decide and document which component raises each of the eight currently unused codes. Until that decision is written down, error handling is untestable, because there is no agreed way to produce the error.

Finally, the action column in the specification's own error table is written for the scientist at the bench. Those actions are outside the scope of this book. Table 4-3 restates, for each code, only the record-keeping and operational response: what the intake service does, what gets written, and who is notified. Every scientific decision is referred to the investigator and to the institutional biosafety and ethics committees.

Table 4-3. Error code → meaning → the caller's record-keeping and operational action
Code Enumeration name Raised in the reference SDK? Record-keeping and operational action
C001NO_PAM_FOUNDNoTreat as a failed intake attempt: log the request digest and the failure, create no protocol record, return the request to the submitter. The scientific question goes to the investigator.
C002HIGH_OFF_TARGET_RISKNo — the validator emits a text error insteadRetain the validation envelope, block the lifecycle transition to execution (Chapter 5 §5.1), and route the record to the safety officer for review.
C003LOW_PREDICTED_EFFICIENCYNoWarning class: do not block, store the value with its tool version, and surface it in the institutional dashboard rather than in the approval path.
C004INVALID_SEQUENCEYes — two input checksInput-validation failure at the boundary. Return to the submitter with the offending field, log the request digest, create no record, and never partially persist the call.
C005DELIVERY_INCOMPATIBLENoIntake rejection naming the field concerned; the compatibility judgement itself is out of scope and belongs to the investigator.
C006ETHICS_VIOLATIONNo — the validator emits a text error insteadHard block. Notify the IBC/IRB secretariat, retain the attempt as an audit event, and require an approval reference before any override (Chapter 8 §8.3).
C007INVALID_PARAMETERSYes — two input checks in the efficiency callReject the submission. Never store a derived percentage computed from input the tool refused; log the counts that were rejected.
C008GENOME_NOT_FOUNDNoSeparate the two causes. An assembly label that does not resolve is a referential failure of the document (Chapter 3 §3.4) and goes back to the submitter; missing or broken reference data on the operator side is a configuration fault — alert the operator, retry after the reference data is fixed, and do not record it against the protocol as a defect.
C009OFF_TARGET_ANALYSIS_FAILEDNoRecord the analysis as not performed — which is distinct from performed and empty — and never let a failed run leave the corresponding attestation flag set to true.
C010VALIDATION_FAILEDNoReserve strictly for "the validator could not run". A validator that ran and found the protocol invalid is reported by the validation result, not by this code; conflating the two makes outage and non-compliance indistinguishable in reports.

The distinction drawn in the last row is worth generalizing, because it is the one that most often corrupts compliance reporting. "The check did not run" and "the check ran and failed" are different facts with different consequences, and a record system that stores them in the same field will, over a year, produce a dashboard that cannot be trusted in either direction.

4.5.1 Result versus throwing

The SDK exports a discriminated-union result type and an asynchronous variant of it. Neither is used by any function in the reference implementation: every entry point signals failure by throwing. The consequence for record intake is concrete. A batch that validates two hundred submitted protocols aborts on the first bad one unless the caller wraps each call individually, and a partially processed batch is the classic way to end up with records whose provenance nobody can reconstruct. The adapter is trivial, and the institution's boundary is the right place for it:

import type { Result } from '@wia/bio-014';
import { CRISPRProtocolError } from '@wia/bio-014';

export function tryCall<T>(fn: () => T): Result<T, CRISPRProtocolError> {
  try {
    return { success: true, data: fn() };
  } catch (err) {
    if (err instanceof CRISPRProtocolError) {
      return { success: false, error: err };
    }
    throw err; // Not our contract: never disguise an unknown fault.
  }
}

Two rules govern the catch block. It never swallows a failure that bears on compliance: a bare catch around a validator call turns a protocol that should have been blocked into a protocol with no validation record at all, which is worse than an outage because it looks like success. And a failure entry carries the same envelope as a success entry — the same tool version, actor, input digest, and clock source — because a reviewer asking what happened on a given day must be able to see the attempts that did not produce a record. Where the surface is exposed over HTTP, the error code should be carried in a structured problem document rather than in a prose body; RFC 9457, which obsoletes RFC 7807, defines the appropriate format, and Chapter 6 §6.2 places it in the transport profile.

4.6 The CLI as the automation seam

The reference command-line tool exposes seven commands: guide design, off-target prediction, efficiency calculation, protocol validation, template generation, a version banner, and help. Its options are the ones its own help text lists — a target sequence and PAM type for design; guide, genome and maximum-mismatch for prediction; edited and total counts for efficiency; a protocol file for validation; an output path for the template. As an automation seam its behaviour matters more than its feature list, and five behaviours are load-bearing.

Every recognized command exits with status zero. The script ends with a single unconditional successful exit; the validation routine does return a failure status when the protocol file is missing, but that status is discarded by the dispatch block that called it. Only an unrecognized command name exits non-zero. Since the POSIX convention codified in IEEE Std 1003.1 is that a zero exit status means success, a continuous-integration job that gates on this tool passes even when the file it was asked to validate does not exist. The operational rule is therefore blunt: do not gate any pipeline on this tool's exit status. Gate on the SDK, or on parsing the tool's output, until a release propagates the status — the fix is to return the routine's status from the dispatch block rather than exiting zero unconditionally.

Every command carries built-in defaults, and unrecognized options are silently discarded. The argument loops match the documented options and fall through to a bare shift for anything else. A mistyped option name therefore leaves the built-in default in place and produces no diagnostic at all, so the efficiency command asked about a mistyped total will confidently report a figure derived from its default counts. For intake automation the rule is to validate arguments before the call, and to echo the effective parameters into the record so that a reader can see what the tool was actually given rather than what the operator intended.

Output is human-formatted coloured text, not a machine format. The tool writes section headings and status lines with terminal escape sequences to standard output; there is no JSON mode. The structured-output requirement in this book's programme is therefore a recommendation, not a description of current behaviour: a future release should offer an option emitting exactly one JSON document per invocation, conforming to RFC 8259, with the record envelope fields of §4.1 included. Until then, parsing the coloured output is brittle and the SDK is the correct automation surface.

The validation command is a demonstration stub. It checks only that the named file exists and then prints a fixed sequence of check lines — including a fixed biosafety-level line and a fixed overall assessment — regardless of what the file contains; its own source comment states that a real implementation would parse the JSON. Its output is consequently not evidence of anything about the protocol, and it must never be attached to an approval package, a conformance claim, or an audit response. If one sentence from this chapter reaches an institution's operating procedure, it should be this one.

Template generation writes a fixed skeleton whose every value is a placeholder. Chapter 3 restates that point for the JSON document itself. The template-then-fill sequence nonetheless remains the right intake workflow, for a reason that has nothing to do with the values: the template fixes the field set and the snake_case spelling of the serialized form, so a submitter cannot invent field names. The filled template then goes to the SDK validator — not to the CLI validator.

Table 4-4. Reference CLI commands read as automation surfaces
Command Behaviour when its option is omitted Output form Exit status Usable as evidence?
design-grnaRuns on a built-in placeholder sequenceColoured text0No — placeholder input, no envelope
predict-offtargetsRuns on a built-in placeholder guide and assemblyColoured text0No — demonstration output
calc-efficiencyRuns on built-in default countsColoured text0No — the counts may not be the operator's
validateLooks for a default file nameColoured text, fixed content0, even when the file is missingNo — fixed output, no parsing
generate-templateWrites to a default file nameA JSON file of placeholders0Not evidence, but a valid intake starting point
version—Coloured text banner0Only if the version string is captured into an envelope
help—Coloured text0No

4.6.1 A transcript, and what it does not prove

The following transcript shows the intake sequence with the operator's reasoning in comments. Values are placeholders.

$ wia-bio-014 generate-template --output intake/PROTO-PLACEHOLDER.json
# Writes a skeleton. Every value inside is a placeholder, including the
# example gene, coordinates and delivery parameters. Nothing in it is a
# recommended configuration.

$ vi intake/PROTO-PLACEHOLDER.json
# The submitter replaces placeholders with the project's own values.
# Field names and their snake_case spelling are NOT changed: the template
# is the authority on the field set (Chapter 3 §3.1).

$ wia-bio-014 validate --protocol intake/PROTO-PLACEHOLDER.json
# Prints a fixed list of check lines and a fixed overall assessment.
# It did not parse the file. Do NOT record this output.

$ echo $?
0
# Zero even for a missing file: the status is not propagated.
# Therefore: never gate a pipeline on this command.

$ node ./intake/validate-as-evidence.mjs intake/PROTO-PLACEHOLDER.json
# The wrapper of §4.3 calls the SDK validator and writes ONE envelope
# to the record store, with tool version, actor, input digest and flags.
# This is the artifact the approval workflow consumes.

4.6.2 Where the call sits

The sequence below places the SDK inside an institutional intake path. The SDK is an in-process library with no state, so it appears as a synchronous step with no storage of its own; every arrow that persists anything belongs to the institution.

submitter        intake service        SDK (in-process)      record store      audit log
    |                   |                      |                   |               |
    |-- filled JSON -->|                      |                   |               |
    |                   |-- parse + schema -->|                   |               |
    |                   |   (structural layer, Chapter 3 §3.4)     |               |
    |                   |<-- typed protocol ---|                   |               |
    |                   |                      |                   |               |
    |                   |-- validateProtocol(flags) -->|          |               |
    |                   |<-- ValidationResult (no flags echoed) ---|               |
    |                   |                      |                   |               |
    |                   |-- build envelope: tool+version, actor,    |               |
    |                   |   input digest, flags, clock source ------>|              |
    |                   |                      |         store envelope (append)   |
    |                   |                      |                   |-- event ---->|
    |                   |                      |                   |   who/what/when
    |<-- decision ------|                      |                   |               |
    |   (valid | blocked, with the envelope id as the citation)     |               |

Note what the diagram does not contain: the SDK never touches the record store, never reads the actor, and never writes an audit event. Those three arrows exist only because the institution drew them.

4.7 Seven pitfalls at the API layer

Each of the following has been observable in the contracts described above, and each produces a record that survives an ordinary code review and fails an audit.

  1. Storing a score without its input snapshot. A stored number whose request was discarded cannot be recomputed, compared, or defended. The envelope's input digest is the minimum; the full canonical request is better.
  2. Treating free text as machine-readable. Warning, error, and recommendation strings are human sentences whose wording may change between versions. Map them to local codes at intake and keep the original text verbatim beside the code.
  3. Creating records without an idempotency key. Nothing in the surface is idempotent, and the simulation identifier is generated from a clock reading and a random suffix. A retry after a timeout therefore creates a second record that no query can recognize as a duplicate. Key every creation on a digest of the canonical input plus the tool version plus the actor.
  4. Trusting derived defaults as if the caller had stated them. The assembly label derived from the organism field, and the validation flags that default to enabled, both end up in records as though a human had asserted them. Record what the submitter said; record separately what the tool inferred.
  5. Swallowing exceptions around compliance checks. A caught-and-ignored validator failure produces a protocol with no validation record, which reads downstream as an absence rather than as a fault. Log the failure with the same envelope as a success.
  6. Mixing predicted and measured values in one field family. Simulation outputs and sequencing-derived outcomes must occupy different record types, joined for reporting and never merged in storage.
  7. Automating against a demonstration stub, or against an exit status the tool never sets. Both failures look like a green pipeline. Before wiring any tool into an approval path, confirm that it parses its input and that it reports failure in a way the pipeline can observe.

Chapter summary. The reference SDK is a stateless library of five record services; the reference CLI is a demonstration wrapper around them. Neither carries identity, persistence, provenance, or an audit hook. Everything that makes a call's output into a record — tool version, actor, input digest, request flags, clock source, idempotency key, coded findings — is built by the institution, in the envelope of §4.1. The following chapters take that envelope as given: Chapter 5 puts it into a lifecycle with approval gates and an audit trail, Chapter 6 places it on a transport and behind access control, and Chapter 7 turns a set of envelopes into a conformance evidence package.

Scientific and laboratory-safety content: consult your IBC/IRB and the official biosafety and regulatory authorities (KDCA and the LMO Act authorities, WHO, the NIH Guidelines, and your national regulator). This chapter documents record fields, call contracts, and error handling only; it gives no laboratory, design, or safety guidance.