Skip to content

Commit 9c259fe

Browse files
committed
update docs
1 parent 4f5ae96 commit 9c259fe

File tree

1 file changed

+67
-75
lines changed

1 file changed

+67
-75
lines changed

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

Lines changed: 67 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -9,59 +9,41 @@ sidebar:
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

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).
1413

1514
This page is organized in two halves:
1615

1716
- **Instrument Model**: What an instrument is, how it narrows by `kind`, how scalar vs series instruments differ, and how schema-driven typing works.
1817
- **Instrument Sources and Bundling**: How multi-file instrument sources become a single executable bundle that can be stored and executed at runtime.
1918

20-
### Instruments Are JavaScript Objects
19+
### Instrument Model
2120

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.
21+
#### Runtime Representation
2322

24-
### TypeScript: Compile-Time Structure (Not Runtime)
23+
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
2526

2627
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.
2728

2829
The type system is designed to enforce and enable things like:
2930

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.
3334

3435
These rules are implemented using TypeScript features such as:
3536

3637
- [discriminated unions](https://www.typescriptlang.org/docs/handbook/2/narrowing.html#discriminated-unions)
3738
- [conditional types](https://www.typescriptlang.org/docs/handbook/2/conditional-types.html)
3839

39-
It is important to understand that **TypeScript types do not exist at runtime**. Their role is to help authors and maintainers catch mistakes early.
40-
41-
### Public Runtime API
42-
43-
In the runtime environment, users typically define instruments by importing helper functions from the runtime v1 entrypoint:
44-
45-
```ts
46-
import { defineInstrument, defineSeriesInstrument } from '/runtime/v1/@opendatacapture/runtime-core';
47-
```
48-
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:
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.
5941

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

6244
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.
6345

64-
Two ideas drive almost everything:
46+
Two discriminators drive most of the design:
6547

6648
1. `kind` selects _what type of instrument this is_, and TypeScript uses that to narrow the allowed shape of `content` (and other fields).
6749
2. `language` selects _how UI-facing values are represented_ (single value vs per-language map).
@@ -100,16 +82,13 @@ type BaseInstrument = {
10082
clientDetails?: {
10183
/* subject-facing metadata; localized fields depend on language */
10284
};
103-
tags: {
104-
/* localized depending on language */
105-
};
10685
content: {
10786
/* becomes specific after narrowing by kind */
10887
};
10988
};
11089
```
11190

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.
11392

11493
#### How Narrowing Works: `kind` Drives Shape
11594

@@ -145,15 +124,15 @@ The benefit is that the type system becomes _predictable_ for both authors and c
145124

146125
Scalar instruments are those that can be completed. They:
147126

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
152131

153132
Series instruments are orchestration containers. They:
154133

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)
157136

158137
##### Quick Comparison
159138

@@ -165,39 +144,38 @@ Series instruments are orchestration containers. They:
165144
| Can define `measures` | Yes | No |
166145
| `content` describes UI | Yes | No |
167146

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

170149
A central design rule is:
171150

172151
- The instrument output `data` type is derived from `validationSchema`.
173152

174153
This is why `defineInstrument` is so important. It creates a pipeline where one authoring artifact (the schema) becomes the source of truth for:
175154

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
179158

180159
Conceptually, the inference pipeline is:
181160

182161
1. You choose `kind` (`FORM` or `INTERACTIVE`).
183162
2. You provide a Zod schema as `validationSchema`.
184163
3. TypeScript infers the output type from that schema.
185164
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)
189167

190168
This accomplishes two things:
191169

192170
1. Runtime validation and compile-time typing stay aligned by construction.
193171
2. Everything that depends on `TData` (measures, form field typing, etc.) becomes automatically type-safe.
194172

195-
### Localization Model: One Rule Used Everywhere
173+
#### Localization Model: One Rule Used Everywhere
196174

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

199177
- **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`).
201179
- **Multilingual**: `language` is an array of languages (e.g. `['en', 'fr']`).
202180
- UI fields become per-language objects (e.g. `title: { en: string; fr: string }`).
203181

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

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

223-
### Instruments by Kind (Narrowed View)
201+
#### Instruments by Kind (Narrowed View)
224202

225-
#### FORM: Declarative Form Instruments
203+
##### Form Instruments
226204

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

@@ -231,7 +209,7 @@ Form instruments are scalar instruments whose `content` is a declarative descrip
231209

232210
Practically, you author:
233211

234-
- a Zod schema for the output data (e.g. `{ overallHappiness: number }`)
212+
- A Zod schema for the output data (e.g. `{ overallHappiness: number }`)
235213
- `content` with fields keyed by those data keys (e.g. `overallHappiness: { kind: 'number', ... }`)
236214

237215
The form system also supports two authoring styles:
@@ -246,19 +224,19 @@ Dynamic behavior is modeled explicitly:
246224

247225
The type system uses the output data type to ensure:
248226

249-
- the field kind matches the expected value type
250-
- dynamic field rendering returns a compatible field definition for that value type
227+
- The field kind matches the expected value type
228+
- Dynamic field rendering returns a compatible field definition for that value type
251229

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

254-
#### INTERACTIVE: Imperative Interactive Instruments
232+
##### Interactive Instruments
255233

256234
Interactive instruments are scalar instruments whose `content` is code-driven rather than declarative.
257235

258236
The contract is:
259237

260238
- `content.render(done)` runs your instrument UI logic.
261-
- when finished, you call `done(data)` to complete with an output payload.
239+
- When finished, you call `done(data)` to complete with an output payload.
262240

263241
Two notable constraints are enforced by types:
264242

@@ -267,12 +245,12 @@ Two notable constraints are enforced by types:
267245

268246
Interactive content may also provide:
269247

270-
- optional `html` scaffolding
248+
- Optional `html` scaffolding
271249
- `meta` tags
272250
- `staticAssets` (key/value asset map)
273-
- a restricted head injection surface for legacy script/style (`__injectHead`)
251+
- A restricted head injection surface for legacy script/style (`__injectHead`)
274252

275-
#### SERIES: Instrument Orchestration
253+
##### Series Instruments
276254

277255
Series instruments are not scalar. Their job is to _refer_ to scalar instruments, not define output themselves.
278256

@@ -285,10 +263,10 @@ The runtime can interpret those references as "run these scalar instruments in o
285263

286264
The type system keeps the distinction sharp:
287265

288-
- series `content` is references, not executable UI
289-
- series has no schema/measures/internal identity
266+
- Series `content` is references, not executable UI
267+
- Series has no schema/measures/internal identity
290268

291-
### Measures: Derived Values From Scalar Output
269+
#### Measures: Derived Values From Scalar Output
292270

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

@@ -302,20 +280,34 @@ There are two measure modes:
302280

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

305-
### Authoring Instruments (Recommended)
283+
### Authoring Instruments
306284

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

289+
#### Public Runtime API
290+
291+
In the runtime environment, users should define instruments by importing helper functions from the runtime v1 entrypoint:
292+
293+
```ts
294+
import { defineInstrument, defineSeriesInstrument } from '/runtime/v1/@opendatacapture/runtime-core';
295+
```
296+
297+
Scalar instruments (`FORM` and `INTERACTIVE`) define a validation schema using Zod:
298+
299+
```ts
300+
import { z } from '/runtime/v1/zod@3.x';
301+
```
302+
311303
#### `defineInstrument` (Scalar)
312304

313305
Use `defineInstrument(...)` for `FORM` and `INTERACTIVE` instruments.
314306

315307
It is recommended because it:
316308

317-
- sets runtime-controlled fields (notably the runtime version marker)
318-
- makes TypeScript infer the output `data` type from `validationSchema`
309+
- Sets runtime-controlled fields (notably the runtime version marker)
310+
- Makes TypeScript infer the output `data` type from `validationSchema`
319311

320312
Minimal form example:
321313

@@ -419,7 +411,7 @@ export default defineSeriesInstrument({
419411
});
420412
```
421413

422-
### Repo-Only Constraint: License Narrowing
414+
#### Repo-Only Constraint: License Narrowing
423415

424416
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:
425417

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

429421
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.
430422

431-
### Instrument Sources: Why Instruments Are Bundled
423+
### Instrument Sources and Bundling
424+
425+
#### Instrument Sources
432426

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

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

443437
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.
444438

445-
### Instrument Bundler
439+
#### Instrument Bundler
446440

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

@@ -465,12 +459,10 @@ Once found, the bundler injects a tiny entry module into esbuild. For example, i
465459

466460
The effect is that the instrument's default export becomes available under a known name (`__exports`) in the bundle.
467461

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'`)
474466

475467
The bundling step produces:
476468

@@ -504,7 +496,7 @@ For example:
504496
})();
505497
```
506498

507-
### Storage and Execution
499+
#### Storage and Execution
508500

509501
The final bundle output is stored in the database.
510502

0 commit comments

Comments
 (0)