Skip to content

Commit 4f5ae96

Browse files
committed
update docs
1 parent dae6427 commit 4f5ae96

File tree

1 file changed

+47
-46
lines changed

1 file changed

+47
-46
lines changed

docs/en/4-concepts/4.1-instruments.mdx

Lines changed: 47 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ sidebar:
55
order: 1
66
---
77

8-
## Overview
8+
### Overview
99

1010
In Open Data Capture (ODC), an instrument is the unit of data collection: it defines _what the user sees_, _what data is produced_, and _how that data is validated_.
1111

@@ -17,19 +17,19 @@ This page is organized in two halves:
1717
- **Instrument Model**: What an instrument is, how it narrows by `kind`, how scalar vs series instruments differ, and how schema-driven typing works.
1818
- **Instrument Sources and Bundling**: How multi-file instrument sources become a single executable bundle that can be stored and executed at runtime.
1919

20-
## Instruments Are JavaScript Objects
20+
### Instruments Are JavaScript Objects
2121

2222
At runtime, an instrument is a plain [JavaScript object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object). It is _not_ a class instance, and it does not require any runtime type metadata; what makes an object a valid instrument is that it matches the expected shape used by the ODC runtime and UI.
2323

24-
## TypeScript: Compile-Time Structure (Not Runtime)
24+
### TypeScript: Compile-Time Structure (Not Runtime)
2525

2626
Although instruments are plain objects at runtime, ODC uses [TypeScript](https://www.typescriptlang.org/) to provide static type checking and compile-time data-shape enforcement.
2727

2828
The type system is designed to enforce and enable things like:
2929

30-
- **Discriminated narrowing**: based on `kind`, the type of `content` (and other fields) narrows to the correct variant.
31-
- **Schema-first data typing**: the instrument output `data` type is inferred from the Zod validation schema.
32-
- **Localization shaping**: based on `language`, UI-facing values become either a plain value or a per-language mapping.
30+
- **Discriminated Narrowing**: Based on `kind`, the type of `content` (and other fields) narrows to the correct variant.
31+
- **Schema-Inferred Data Typing**: The instrument output `data` type is inferred from the Zod validation schema.
32+
- **Localization Shaping**: Based on `language`, UI-facing values become either a single value or a per-language mapping.
3333

3434
These rules are implemented using TypeScript features such as:
3535

@@ -38,26 +38,26 @@ These rules are implemented using TypeScript features such as:
3838

3939
It is important to understand that **TypeScript types do not exist at runtime**. Their role is to help authors and maintainers catch mistakes early.
4040

41-
## Public Runtime API
41+
### Public Runtime API
4242

4343
In the runtime environment, users typically define instruments by importing helper functions from the runtime v1 entrypoint:
4444

4545
```ts
4646
import { defineInstrument, defineSeriesInstrument } from '/runtime/v1/@opendatacapture/runtime-core';
4747
```
4848

49-
Instruments can also import approved third-party libraries from the runtime. For example, all instruments must define a validation schema using Zod:
49+
Instruments can also import approved third-party libraries from the runtime. For example, scalar instruments (`FORM` and `INTERACTIVE`) define a validation schema using Zod:
5050

5151
```ts
5252
import { z } from '/runtime/v1/zod@3.x';
5353
```
5454

55-
Although an instrument can be technically be defined without these helpers, `defineInstrument` and `defineSeriesInstrument` are intentionally designed to make the type system usable in practice:
55+
Although an instrument can technically be defined without these helpers, `defineInstrument` and `defineSeriesInstrument` are intentionally designed to make the type system usable in practice:
5656

57-
- they set internal, runtime-controlled fields (like the runtime version marker)
58-
- they make TypeScript inference flow in the right direction to provide a better developer experience
57+
- They set internal, runtime-controlled fields (like the runtime version marker)
58+
- They make TypeScript inference flow in the right direction to provide a better developer experience
5959

60-
## Mental Model: "Instrument" Is a Discriminated Family
60+
### Mental Model: "Instrument" Is a Discriminated Family
6161

6262
At the highest level, an `Instrument` is not one shape: it is a _family_ of shapes that share a common envelope and then diverge based on a discriminator.
6363

@@ -71,7 +71,7 @@ There is also a higher-level split:
7171
- **Scalar instruments**: "completable" instruments that produce a single output payload, validated by a schema.
7272
- **Series instruments**: "compositional" instruments that reference multiple scalar instruments by identity.
7373

74-
### The Base Instrument
74+
#### The Base Instrument
7575

7676
Every instrument, regardless of kind, carries:
7777

@@ -111,7 +111,7 @@ type BaseInstrument = {
111111

112112
The important point is not the exact properties; it is that the system is deliberately set up so that _once `kind` is known_, everything downstream becomes more specific.
113113

114-
### How Narrowing Works: `kind` Drives Shape
114+
#### How Narrowing Works: `kind` Drives Shape
115115

116116
The type system is built around the idea:
117117

@@ -122,7 +122,7 @@ The type system is built around the idea:
122122
Therefore, in the codebase, consumers can narrow the type of `Instrument` based on `kind`:
123123

124124
```ts
125-
import type { AnyInstrument } from '/runtime/v1/@opendatacapture/runtime-core/index.js';
125+
import type { AnyInstrument } from '/runtime/v1/@opendatacapture/runtime-core';
126126

127127
export function handleInstrument(instrument: AnyInstrument) {
128128
switch (instrument.kind) {
@@ -141,7 +141,7 @@ export function handleInstrument(instrument: AnyInstrument) {
141141

142142
The benefit is that the type system becomes _predictable_ for both authors and consumers: `kind` selects the variant, and TypeScript follows.
143143

144-
### Scalar vs Series Instruments
144+
#### Scalar vs Series Instruments
145145

146146
Scalar instruments are those that can be completed. They:
147147

@@ -155,7 +155,7 @@ Series instruments are orchestration containers. They:
155155
- `content` is an ordered list of scalar instrument identities (the same `{ edition, name }` shape used by scalar `internal`)
156156
- do not define `validationSchema`, `measures`, or `internal` (they are not directly completed)
157157

158-
#### Quick Comparison
158+
##### Quick Comparison
159159

160160
| Capability | Scalar (`FORM` / `INTERACTIVE`) | Series (`SERIES`) |
161161
| ------------------------------ | ------------------------------- | ----------------- |
@@ -165,7 +165,7 @@ Series instruments are orchestration containers. They:
165165
| Can define `measures` | Yes | No |
166166
| `content` describes UI | Yes | No |
167167

168-
## Where "Data" Comes From: Schema as the Source of Truth
168+
### Where "Data" Comes From: Schema as the Source of Truth
169169

170170
A central design rule is:
171171

@@ -192,7 +192,7 @@ This accomplishes two things:
192192
1. Runtime validation and compile-time typing stay aligned by construction.
193193
2. Everything that depends on `TData` (measures, form field typing, etc.) becomes automatically type-safe.
194194

195-
## Localization Model: One Rule Used Everywhere
195+
### Localization Model: One Rule Used Everywhere
196196

197197
ODC supports unilingual and multilingual instruments. The instrument's `language` value selects the mode:
198198

@@ -220,9 +220,9 @@ InstrumentUIOption<TLanguage, TValue> =
220220

221221
The practical implication is that changing `language` changes the required structure of many fields.
222222

223-
## Instruments by Kind (Narrowed View)
223+
### Instruments by Kind (Narrowed View)
224224

225-
### FORM: Declarative Form Instruments
225+
#### FORM: Declarative Form Instruments
226226

227227
Form instruments are scalar instruments whose `content` is a declarative description of fields. The key design is "data drives UI":
228228

@@ -251,7 +251,7 @@ The type system uses the output data type to ensure:
251251

252252
This is a compile-time guarantee that the form authoring surface stays consistent with the instrument's output.
253253

254-
### INTERACTIVE: Imperative Interactive Instruments
254+
#### INTERACTIVE: Imperative Interactive Instruments
255255

256256
Interactive instruments are scalar instruments whose `content` is code-driven rather than declarative.
257257

@@ -272,7 +272,7 @@ Interactive content may also provide:
272272
- `staticAssets` (key/value asset map)
273273
- a restricted head injection surface for legacy script/style (`__injectHead`)
274274

275-
### SERIES: Instrument Orchestration
275+
#### SERIES: Instrument Orchestration
276276

277277
Series instruments are not scalar. Their job is to _refer_ to scalar instruments, not define output themselves.
278278

@@ -288,7 +288,7 @@ The type system keeps the distinction sharp:
288288
- series `content` is references, not executable UI
289289
- series has no schema/measures/internal identity
290290

291-
## Measures: Derived Values From Scalar Output
291+
### Measures: Derived Values From Scalar Output
292292

293293
Scalar instruments can define `measures`, which are named derived values displayed/used by the system.
294294

@@ -302,13 +302,13 @@ There are two measure modes:
302302

303303
Measures are also localized via the same language rule (measure labels are single strings or per-language maps depending on `language`).
304304

305-
## Authoring Instruments (Recommended)
305+
### Authoring Instruments (Recommended)
306306

307307
It is possible to export a plain object as an Instrument (because Instruments are just JavaScript objects at runtime).
308308
However, most instruments should use the public helper functions because they set runtime-controlled fields and make
309309
TypeScript inference and narrowing work as intended.
310310

311-
### `defineInstrument` (Scalar)
311+
#### `defineInstrument` (Scalar)
312312

313313
Use `defineInstrument(...)` for `FORM` and `INTERACTIVE` instruments.
314314

@@ -320,14 +320,18 @@ It is recommended because it:
320320
Minimal form example:
321321

322322
```ts
323-
import { defineInstrument } from '/runtime/v1/@opendatacapture/runtime-core/index.js';
323+
import { defineInstrument } from '/runtime/v1/@opendatacapture/runtime-core';
324324
import { z } from '/runtime/v1/zod@3.x';
325325

326326
export default defineInstrument({
327327
kind: 'FORM',
328328
language: 'en',
329329
tags: ['Example'],
330330
internal: { edition: 1, name: 'HAPPINESS_QUESTIONNAIRE' },
331+
clientDetails: {
332+
estimatedDuration: 1,
333+
instructions: ['Please answer based on your current feelings.']
334+
},
331335
content: {
332336
overallHappiness: {
333337
kind: 'number',
@@ -341,9 +345,7 @@ export default defineInstrument({
341345
details: {
342346
title: 'Happiness Questionnaire',
343347
description: 'A questionnaire about happiness.',
344-
license: 'Apache-2.0',
345-
estimatedDuration: 1,
346-
instructions: ['Please answer based on your current feelings.']
348+
license: 'Apache-2.0'
347349
},
348350
measures: null,
349351
validationSchema: z.object({
@@ -363,6 +365,10 @@ export default defineInstrument({
363365
language: 'en',
364366
tags: ['Example'],
365367
internal: { edition: 1, name: 'CLICK_THE_BUTTON_TASK' },
368+
clientDetails: {
369+
estimatedDuration: 1,
370+
instructions: ['Please click the button when you are done.']
371+
},
366372
content: {
367373
render(done) {
368374
const start = Date.now();
@@ -379,9 +385,7 @@ export default defineInstrument({
379385
details: {
380386
title: 'Click the Button Task',
381387
description: 'A very simple interactive instrument.',
382-
license: 'Apache-2.0',
383-
estimatedDuration: 1,
384-
instructions: ['Please click the button when you are done.']
388+
license: 'Apache-2.0'
385389
},
386390
measures: null,
387391
validationSchema: z.object({
@@ -390,7 +394,7 @@ export default defineInstrument({
390394
});
391395
```
392396

393-
### `defineSeriesInstrument` (Series)
397+
#### `defineSeriesInstrument` (Series)
394398

395399
Use `defineSeriesInstrument(...)` for `SERIES` instruments.
396400

@@ -415,7 +419,7 @@ export default defineSeriesInstrument({
415419
});
416420
```
417421

418-
## Repo-Only Constraint: License Narrowing
422+
### Repo-Only Constraint: License Narrowing
419423

420424
When this package is compiled inside the ODC repo, a global type (`OpenDataCaptureContext`) marks `isRepo: true`, and the definition type is intersected with an extra requirement:
421425

@@ -424,7 +428,7 @@ When this package is compiled inside the ODC repo, a global type (`OpenDataCaptu
424428

425429
This is a type-level policy hook: it changes authoring constraints without changing runtime behavior. If you are authoring instruments in the instrument playground for your own instance, this will not affect you.
426430

427-
## Instrument Sources: Why Instruments Are Bundled
431+
### Instrument Sources: Why Instruments Are Bundled
428432

429433
A single-file instrument source (like the examples above) is the simplest case, but many instruments are multi-file:
430434

@@ -438,7 +442,7 @@ If instruments were defined directly in the Open Data Capture codebase, adding o
438442

439443
To avoid that, ODC treats "instrument sources" as the authoring units (a set of source files), and a bundling pipeline turns those sources into a single executable artifact.
440444

441-
## Instrument Bundler
445+
### Instrument Bundler
442446

443447
Although we often talk about files, the instrument bundler is platform-agnostic and does not require a filesystem.
444448

@@ -462,7 +466,7 @@ Once found, the bundler injects a tiny entry module into esbuild. For example, i
462466
The effect is that the instrument's default export becomes available under a known name (`__exports`) in the bundle.
463467

464468
Our custom plugin assumes responsibility for resolving all imports found. In this case, we resolve the static
465-
relative import `'./index.js` which exists in the inputs. We then return the content of the index input to
469+
relative import `'./index.js'` which exists in the inputs. We then return the content of the index input to
466470
esbuild for further processing. In this case, esbuild will look for imports in the content of the index,
467471
and pass any results to our resolver. The resolver marks all HTTP imports as external (e.g., `import React from '/runtime/v1/react@18.x'`).
468472
It looks for any static imports inside the inputs and throws an exception if it is not found. If it is found,
@@ -487,24 +491,21 @@ before the `render` method is called.
487491

488492
This new content is then passed into the esbuild transpiler as a
489493
single asynchronous [Immediately Invoked Function Expression](https://developer.mozilla.org/en-US/docs/Glossary/IIFE)
490-
that resolves to a [Promise](https://developer.mozilla.org/en-US/docs/Web/J) of an `Instrument`. This output
494+
that resolves to a [Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) of an `Instrument`. This output
491495
conforms to the [ECMAScript 2022 Language Specification](https://262.ecma-international.org/13.0/) and can be executed in any modern browser.
492-
At this point, the code can be minified and tree shaken (i.e., dead code removed).
496+
At this point, the code can be minified and tree-shaken (i.e., dead code removed).
493497

494498
For example:
495499

496500
```js
497-
`(async () => {
501+
(async () => {
498502
// code with styles injected (if applicable)
499503
return __exports;
500-
} )()`;
504+
})();
501505
```
502506

503-
## Storage and Execution
507+
### Storage and Execution
504508

505509
The final bundle output is stored in the database.
506510

507511
At runtime, it can be evaluated in the global scope (using indirect `eval` or the `Function` constructor) to produce a Promise of an Instrument object, which the runtime can then render/execute.
508-
509-
This bundle is then stored in the database. It can be evaluated in the global scope
510-
(using indirect eval or the `Function` constructor) at runtime.

0 commit comments

Comments
 (0)