You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: docs/en/4-concepts/4.1-instruments.mdx
+67-75Lines changed: 67 additions & 75 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -9,59 +9,41 @@ sidebar:
9
9
10
10
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_.
11
11
12
-
Instruments range from simple forms (e.g., a questionnaire assessing depressive symptoms) to complex interactive tasks
13
-
(e.g., the Stroop Task).
12
+
Instruments range from simple forms (e.g., a questionnaire assessing depressive symptoms) to complex interactive tasks (e.g., the Stroop Task).
14
13
15
14
This page is organized in two halves:
16
15
17
16
-**Instrument Model**: What an instrument is, how it narrows by `kind`, how scalar vs series instruments differ, and how schema-driven typing works.
18
17
-**Instrument Sources and Bundling**: How multi-file instrument sources become a single executable bundle that can be stored and executed at runtime.
19
18
20
-
### Instruments Are JavaScript Objects
19
+
### Instrument Model
21
20
22
-
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.
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 shape expected by the ODC runtime and UI.
24
+
25
+
#### TypeScript: Compile-Time Structure
25
26
26
27
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.
27
28
28
29
The type system is designed to enforce and enable things like:
29
30
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.
31
+
-**Discriminated narrowing**: Based on `kind`, the type of `content` (and other fields) narrows to the correct variant.
32
+
-**Schema-inferred data typing**: The instrument output `data` type is inferred from the Zod validation schema.
33
+
-**Localization shaping**: Based on `language`, UI-facing values become either a single value or a per-language mapping.
33
34
34
35
These rules are implemented using TypeScript features such as:
Instruments can also import approved third-party libraries from the runtime. For example, scalar instruments (`FORM` and `INTERACTIVE`) define a validation schema using Zod:
50
-
51
-
```ts
52
-
import { z } from'/runtime/v1/zod@3.x';
53
-
```
54
-
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:
56
-
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
40
+
It is important to understand that **TypeScript types do not exist at runtime**. Their role is to help instrument authors catch mistakes early.
59
41
60
-
### Mental Model: "Instrument" Is a Discriminated Family
42
+
####Mental Model: "Instrument" Is a Discriminated Family
61
43
62
44
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.
63
45
64
-
Two ideas drive almost everything:
46
+
Two discriminators drive most of the design:
65
47
66
48
1.`kind` selects _what type of instrument this is_, and TypeScript uses that to narrow the allowed shape of `content` (and other fields).
67
49
2.`language` selects _how UI-facing values are represented_ (single value vs per-language map).
@@ -100,16 +82,13 @@ type BaseInstrument = {
100
82
clientDetails?: {
101
83
/* subject-facing metadata; localized fields depend on language */
102
84
};
103
-
tags: {
104
-
/* localized depending on language */
105
-
};
106
85
content: {
107
86
/* becomes specific after narrowing by kind */
108
87
};
109
88
};
110
89
```
111
90
112
-
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.
91
+
The important point is not the exact properties; it is that the system is deliberately set up so that _once `kind`and `language` are known_, everything downstream becomes more specific.
113
92
114
93
#### How Narrowing Works: `kind` Drives Shape
115
94
@@ -145,15 +124,15 @@ The benefit is that the type system becomes _predictable_ for both authors and c
145
124
146
125
Scalar instruments are those that can be completed. They:
147
126
148
-
-have a stable `internal` identity (`edition` + `name`)
149
-
-define a `validationSchema` (Zod v3 or v4)
150
-
-produce_one_ output payload (`data`) whose static type is derived from the schema output type
151
-
-can define `measures` derived from that output
127
+
-Have a stable `internal` identity (`edition` + `name`)
128
+
-Define a `validationSchema` (Zod v3 or v4)
129
+
-Produce_one_ output payload (`data`) whose static type is derived from the schema output type
130
+
-Can define `measures` derived from that output
152
131
153
132
Series instruments are orchestration containers. They:
154
133
155
-
-`content` is an ordered list of scalar instrument identities (the same `{ edition, name }` shape used by scalar `internal`)
156
-
-do not define `validationSchema`, `measures`, or `internal` (they are not directly completed)
134
+
-Define an ordered list of scalar instrument identities (the same `{ edition, name }` shape used by scalar `internal`)
135
+
-Do not define `validationSchema`, `measures`, or `internal` (they are not directly completed)
157
136
158
137
##### Quick Comparison
159
138
@@ -165,39 +144,38 @@ Series instruments are orchestration containers. They:
165
144
| Can define `measures`| Yes | No |
166
145
|`content` describes UI | Yes | No |
167
146
168
-
### Where "Data" Comes From: Schema as the Source of Truth
147
+
####Where "Data" Comes From: Schema as the Source of Truth
169
148
170
149
A central design rule is:
171
150
172
151
- The instrument output `data` type is derived from `validationSchema`.
173
152
174
153
This is why `defineInstrument` is so important. It creates a pipeline where one authoring artifact (the schema) becomes the source of truth for:
175
154
176
-
-what the runtime will validate
177
-
-what TypeScript believes the output type is
178
-
-what downstream parts of the instrument are allowed to do
155
+
-What the runtime will validate
156
+
-What TypeScript believes the output type is
157
+
-What downstream parts of the instrument (e.g., form fields) are allowed to be
179
158
180
159
Conceptually, the inference pipeline is:
181
160
182
161
1. You choose `kind` (`FORM` or `INTERACTIVE`).
183
162
2. You provide a Zod schema as `validationSchema`.
184
163
3. TypeScript infers the output type from that schema.
185
164
4. That inferred type flows into:
186
-
- form content typing ("data drives UI")
187
-
- measures (refs and computed values)
188
-
- interactive completion payloads (`done(data)`)
165
+
- Scalar instrument content ("data drives UI")
166
+
- Scalar instrument measures (refs and computed values)
189
167
190
168
This accomplishes two things:
191
169
192
170
1. Runtime validation and compile-time typing stay aligned by construction.
193
171
2. Everything that depends on `TData` (measures, form field typing, etc.) becomes automatically type-safe.
194
172
195
-
### Localization Model: One Rule Used Everywhere
173
+
####Localization Model: One Rule Used Everywhere
196
174
197
175
ODC supports unilingual and multilingual instruments. The instrument's `language` value selects the mode:
198
176
199
177
-**Unilingual**: `language` is a single language (e.g. `'en'`).
200
-
- UI fields are plain values (e.g. `title: string`).
178
+
- UI fields are single values (e.g. `title: string`).
201
179
-**Multilingual**: `language` is an array of languages (e.g. `['en', 'fr']`).
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:
425
417
@@ -428,7 +420,9 @@ When this package is compiled inside the ODC repo, a global type (`OpenDataCaptu
428
420
429
421
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.
430
422
431
-
### Instrument Sources: Why Instruments Are Bundled
423
+
### Instrument Sources and Bundling
424
+
425
+
#### Instrument Sources
432
426
433
427
A single-file instrument source (like the examples above) is the simplest case, but many instruments are multi-file:
434
428
@@ -442,7 +436,7 @@ If instruments were defined directly in the Open Data Capture codebase, adding o
442
436
443
437
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.
444
438
445
-
### Instrument Bundler
439
+
####Instrument Bundler
446
440
447
441
Although we often talk about files, the instrument bundler is platform-agnostic and does not require a filesystem.
448
442
@@ -465,12 +459,10 @@ Once found, the bundler injects a tiny entry module into esbuild. For example, i
465
459
466
460
The effect is that the instrument's default export becomes available under a known name (`__exports`) in the bundle.
467
461
468
-
Our custom plugin assumes responsibility for resolving all imports found. In this case, we resolve the static
469
-
relative import `'./index.js'` which exists in the inputs. We then return the content of the index input to
470
-
esbuild for further processing. In this case, esbuild will look for imports in the content of the index,
471
-
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'`).
472
-
It looks for any static imports inside the inputs and throws an exception if it is not found. If it is found,
473
-
the content is passed to esbuild and the bundling process continues.
462
+
Our custom plugin assumes responsibility for resolving all imports found:
463
+
464
+
- Relative imports must exist in the inputs (or the bundler throws)
465
+
- HTTP/runtime imports are treated as external (e.g., `import React from '/runtime/v1/react@18.x'`)
474
466
475
467
The bundling step produces:
476
468
@@ -504,7 +496,7 @@ For example:
504
496
})();
505
497
```
506
498
507
-
### Storage and Execution
499
+
####Storage and Execution
508
500
509
501
The final bundle output is stored in the database.
0 commit comments