@case-framework/survey-assistant 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. package/dist/{authoring-references-DbuOKerg.mjs → authoring-references-CmwLc_C8.mjs} +40 -32
  2. package/dist/authoring-references-CmwLc_C8.mjs.map +1 -0
  3. package/dist/capabilities-D55dq-fO.mjs +582 -0
  4. package/dist/capabilities-D55dq-fO.mjs.map +1 -0
  5. package/dist/capabilities-default.d.mts +4 -12
  6. package/dist/capabilities-default.d.mts.map +1 -1
  7. package/dist/capabilities-default.mjs +2 -2
  8. package/dist/capabilities-z95e1mti.d.mts +117 -0
  9. package/dist/capabilities-z95e1mti.d.mts.map +1 -0
  10. package/dist/{constants-B6HzpEsx.d.mts → constants-HL_klgJl.d.mts} +4 -4
  11. package/dist/{constants-B6HzpEsx.d.mts.map → constants-HL_klgJl.d.mts.map} +1 -1
  12. package/dist/{controller-proxy-DEeFl9IA.mjs → controller-proxy-BQ1YsVxJ.mjs} +4 -3
  13. package/dist/controller-proxy-BQ1YsVxJ.mjs.map +1 -0
  14. package/dist/{controller-proxy-D7uQfm5f.d.mts → controller-proxy-Cmzdr9tV.d.mts} +4 -4
  15. package/dist/{controller-proxy-D7uQfm5f.d.mts.map → controller-proxy-Cmzdr9tV.d.mts.map} +1 -1
  16. package/dist/default-D22AKTqE.mjs +233 -0
  17. package/dist/default-D22AKTqE.mjs.map +1 -0
  18. package/dist/{digest-C39WyAWG.d.mts → digest-FnTTDBq2.d.mts} +2 -2
  19. package/dist/digest-FnTTDBq2.d.mts.map +1 -0
  20. package/dist/digest.d.mts +1 -1
  21. package/dist/engine-D950TfvC.mjs +3098 -0
  22. package/dist/engine-D950TfvC.mjs.map +1 -0
  23. package/dist/engine.d.mts +4 -3
  24. package/dist/engine.mjs +3 -3
  25. package/dist/{index-WB5kfnz0.d.mts → index-B8TLBd1G.d.mts} +8 -6
  26. package/dist/index-B8TLBd1G.d.mts.map +1 -0
  27. package/dist/index-CaX3hZmr.d.mts +23813 -0
  28. package/dist/index-CaX3hZmr.d.mts.map +1 -0
  29. package/dist/{index-r5riv4md.d.mts → index-CeUlGkoU.d.mts} +27 -6
  30. package/dist/index-CeUlGkoU.d.mts.map +1 -0
  31. package/dist/{index-D7wT6P3t.d.mts → index-DYHdvSE8.d.mts} +83 -41
  32. package/dist/index-DYHdvSE8.d.mts.map +1 -0
  33. package/dist/lifecycle-DFzxVxpq.d.mts +54346 -0
  34. package/dist/lifecycle-DFzxVxpq.d.mts.map +1 -0
  35. package/dist/{memory-thread-repository-DAnOGIB3.mjs → memory-thread-repository-DmLadygY.mjs} +15 -9
  36. package/dist/{memory-thread-repository-DAnOGIB3.mjs.map → memory-thread-repository-DmLadygY.mjs.map} +1 -1
  37. package/dist/protocol-BQ7uIDYT.mjs +569 -0
  38. package/dist/protocol-BQ7uIDYT.mjs.map +1 -0
  39. package/dist/protocol.d.mts +4 -4
  40. package/dist/protocol.mjs +3 -2
  41. package/dist/{react-C_GY-Fsc.mjs → react-DybJ5pWw.mjs} +17 -11
  42. package/dist/react-DybJ5pWw.mjs.map +1 -0
  43. package/dist/react-integration.d.mts +1 -1
  44. package/dist/react-integration.mjs +1 -1
  45. package/dist/react.d.mts +2 -2
  46. package/dist/react.mjs +2 -2
  47. package/dist/references/assistant-operations.md +190 -215
  48. package/dist/references/core-rules.md +2 -2
  49. package/dist/references/element-types.md +189 -0
  50. package/dist/references/expressions.md +215 -238
  51. package/dist/references/follow-ups.md +151 -0
  52. package/dist/references/localization.md +26 -12
  53. package/dist/references/response-variables.md +3 -3
  54. package/dist/references/rich-text-content.md +22 -43
  55. package/dist/references/source-material-surveys.md +20 -25
  56. package/dist/references/survey-data-model.md +36 -117
  57. package/dist/{request-context-Dg4jSwN1.mjs → request-context-KVh8VjgH.mjs} +4 -4
  58. package/dist/request-context-KVh8VjgH.mjs.map +1 -0
  59. package/dist/server-agent.d.mts +28 -7
  60. package/dist/server-agent.d.mts.map +1 -1
  61. package/dist/server-agent.mjs +33 -25
  62. package/dist/server-agent.mjs.map +1 -1
  63. package/dist/server-runtime.d.mts +6 -5
  64. package/dist/server-runtime.d.mts.map +1 -1
  65. package/dist/server-runtime.mjs +3 -3
  66. package/dist/server-tasks.d.mts +1 -1
  67. package/dist/server-tasks.mjs +1 -1
  68. package/dist/server-tools.d.mts +1 -1
  69. package/dist/server-tools.mjs +1 -1
  70. package/dist/server.d.mts +7 -6
  71. package/dist/server.d.mts.map +1 -1
  72. package/dist/server.mjs +3 -3
  73. package/dist/{tasks-BZEiwgxA.mjs → tasks-Coh5N027.mjs} +16 -15
  74. package/dist/tasks-Coh5N027.mjs.map +1 -0
  75. package/dist/{tasks-B9h3LPam.d.mts → tasks-wb9M-bak.d.mts} +11 -11
  76. package/dist/{tasks-B9h3LPam.d.mts.map → tasks-wb9M-bak.d.mts.map} +1 -1
  77. package/dist/{thread-handlers-Bef0td9N.d.mts → thread-handlers-C3Iya92v.d.mts} +6 -5
  78. package/dist/thread-handlers-C3Iya92v.d.mts.map +1 -0
  79. package/dist/{tools-CEsv-WoO.mjs → tools-D0KBDolR.mjs} +435 -408
  80. package/dist/tools-D0KBDolR.mjs.map +1 -0
  81. package/dist/turn-survey-draft-B34lggSG.d.mts +185 -0
  82. package/dist/turn-survey-draft-B34lggSG.d.mts.map +1 -0
  83. package/dist/ui.d.mts +3 -3
  84. package/dist/ui.d.mts.map +1 -1
  85. package/dist/ui.mjs +35 -23
  86. package/dist/ui.mjs.map +1 -1
  87. package/package.json +7 -7
  88. package/dist/authoring-references-DbuOKerg.mjs.map +0 -1
  89. package/dist/capabilities-CmeeAjEc.d.mts +0 -192
  90. package/dist/capabilities-CmeeAjEc.d.mts.map +0 -1
  91. package/dist/capabilities-DCMA71PP.mjs +0 -58
  92. package/dist/capabilities-DCMA71PP.mjs.map +0 -1
  93. package/dist/controller-proxy-DEeFl9IA.mjs.map +0 -1
  94. package/dist/default-fhSon2RM.mjs +0 -760
  95. package/dist/default-fhSon2RM.mjs.map +0 -1
  96. package/dist/digest-C39WyAWG.d.mts.map +0 -1
  97. package/dist/engine-nXoCWYpQ.mjs +0 -6888
  98. package/dist/engine-nXoCWYpQ.mjs.map +0 -1
  99. package/dist/index-Ciq7vHIs.d.mts +0 -716
  100. package/dist/index-Ciq7vHIs.d.mts.map +0 -1
  101. package/dist/index-D7wT6P3t.d.mts.map +0 -1
  102. package/dist/index-WB5kfnz0.d.mts.map +0 -1
  103. package/dist/index-r5riv4md.d.mts.map +0 -1
  104. package/dist/lifecycle-Dm8u7QFh.d.mts +0 -545
  105. package/dist/lifecycle-Dm8u7QFh.d.mts.map +0 -1
  106. package/dist/protocol-CF4Bh_Ch.mjs +0 -1255
  107. package/dist/protocol-CF4Bh_Ch.mjs.map +0 -1
  108. package/dist/react-C_GY-Fsc.mjs.map +0 -1
  109. package/dist/references/embedded-forms.md +0 -146
  110. package/dist/references/item-types.md +0 -641
  111. package/dist/request-context-Dg4jSwN1.mjs.map +0 -1
  112. package/dist/tasks-BZEiwgxA.mjs.map +0 -1
  113. package/dist/thread-handlers-Bef0td9N.d.mts.map +0 -1
  114. package/dist/tools-CEsv-WoO.mjs.map +0 -1
@@ -1,641 +0,0 @@
1
- # Item Types
2
-
3
- Use current context to confirm which item types are available in the registry. The default registry includes these deterministically creatable types.
4
-
5
- ## group
6
-
7
- Structure/container item:
8
-
9
- ```json
10
- {
11
- "itemType": "group",
12
- "config": {
13
- "items": [],
14
- "shuffleItems": false
15
- }
16
- }
17
- ```
18
-
19
- Root group additionally has `config.isRoot: true`. Non-root groups should not have `isRoot`.
20
- Visible group title uses content key `title`.
21
-
22
- Use a group when its children form a meaningful, independently managed section or require a shared hierarchy. Do not create a group solely to render an isolated heading or a paragraph; use an `infoItem` for explanatory content and a `page-break` when a respondent page boundary is intended. A non-root group must be created empty. In the same validated proposal, create it first and then create each child with that group id in `target.parentItemId`; do not pre-populate `config.items` or use a raw patch for ordinary group creation.
23
-
24
- ## Editor Presentation
25
-
26
- Every item has three distinct naming/presentation surfaces:
27
-
28
- - `editorName`: resolved display name returned by inspection; use the persistent item ID for edits. Response coding belongs to slots, not item names; change it with `update-response-settings`.
29
- - `metadata.itemLabel`: internal editor label, not shown to respondents. Change it with `update-item-label`.
30
- - `metadata.editorItemColor`: editor-only color swatch, not shown to respondents. Change it with `update-item-color`.
31
-
32
- Across editor views, an unqualified request for labels on one or more items/questions means `metadata.itemLabel`, including the visible \"Add label...\" editor control. It does not mean respondent-facing question titles, option labels, or form-field labels unless the user says so. Do not ask for clarification when the requested item set is clear; inspect/search to resolve that set when needed. Clarify only if the request explicitly spans multiple label surfaces.
33
-
34
- `update-item-color.color` must be one of the editor palette values provided in the `survey_change` tool contract. Use it only when the user asks for editor organization or color; it must not be used to imply respondent-facing meaning.
35
-
36
- ## page-break
37
-
38
- Page-break/structural item. It has no response slots. Visible title uses content key `title`.
39
-
40
- ## infoItem
41
-
42
- Non-interactive content item. Config is normally an empty object. Visible body text uses content key `content`.
43
- For plain text, create or update `content` with typed translation operations.
44
- For formatted text, load `assistant-operations` with the operation and item type and use only content surfaces marked `format=rich` in its capability profile. Prefer the model-facing `richTextBlocks` shorthand there; it normalizes to canonical richText `content`. A typed field accepting structured `content` does not by itself guarantee formatted rendering. See rich-text-content.md.
45
-
46
- ## consentItem
47
-
48
- Use one consent item for each independent permission. The item presents a title, supporting subtitle, and full `consentBody` translation, then records an explicit agreement or refusal. Both title and body are needed in each participant language. Use `infoItem` for information that does not ask for a decision.
49
-
50
- Organize the participant-facing content in the question fields:
51
-
52
- - `title`: the main heading identifying the agreement or permission. Prefer the supplied document's main heading when available. This field stays visible on the question card and names the dialog unless `consentTitle` overrides it; do not put the only title inside the body.
53
- - `subtitle`: an optional brief introduction or reading instruction directly below the title. Leave it out when no separate introduction is needed.
54
- - `consentTitle`: optional rich consent-specific heading. Empty inherits `title` in the same locale. Supply only when a distinct local heading is useful; it appears above the inline document or in the dialog summary and dialog heading.
55
- - `consentSubtitle`: optional rich consent-specific introduction, used only with `config.subtitleMode="custom"`.
56
- - `consentBody`: the actual content the participant needs to read before deciding, including the agreement, terms, or explanation of the permission. It is the document itself, not a description, summary, or instruction to read an absent document. Avoid repeating the question title as an opening body heading; retain meaningful section headings, paragraphs, lists, and links within the document. Follow an explicit request to preserve the source verbatim.
57
-
58
- Follow supplied content: preserve its terms and meaning instead of replacing it with generic wording, silently changing its meaning, or adding unrequested provisions. When helping create new wording, be proactive: write useful, concrete draft terms and make reasonable creative choices from the available context. Do the useful work first, then identify the result as a draft and explain material assumptions or missing details in your reply. Do not present invented wording as facts or as a document supplied by the user. Use placeholders sparingly for details the user must supply, while still drafting the substantive content. Do not fill `consentBody` with only a description of a document or a statement agreeing to unspecified terms elsewhere. Ask a focused question before acting only when useful progress is blocked, such as when exact reproduction requires unavailable source text. Missing optional details are a reason to note gaps afterward, not to delay a useful draft.
59
-
60
- Config supports `presentation`: `"inline"` (default) or `"dialog"`, and `subtitleMode`: `"inherit"` (default), `"custom"`, or `"none"`. Subtitle mode applies to all languages. Inherit uses the question subtitle; custom uses `consentSubtitle` for the current locale (empty stays absent); none omits the consent subtitle. The question subtitle remains visible in every mode. Inherited title/subtitle are not repeated inline or in the dialog summary, but are shown inside the dialog. Custom titles also identify the inline consent and dialog summary. Custom subtitles appear above the inline document or inside the dialog, never in the dialog summary.
61
-
62
- Optional plain `acceptLabel` and `declineLabel` translations override choice wording. `unansweredLabel`, `acceptedLabel`, and `declinedLabel` override dialog status; `reviewLabel` and `changeLabel` override dialog actions. Empty values use the host UI defaults. Close, missing-title and unavailable-information messages belong to host player localization; do not create item translations for them. Languages without host translations require host localization for those generic messages. There is no separate decision heading. Without existing response data there is no automatic selection. Consent supports the ordinary initial-response and item-level prefill paths using the boolean slot identified by `config.slotId`. There is no `required` setting or config effects list. Closing the dialog leaves the answer unchanged.
63
-
64
- Config has independent persistent `id` and `slotId`, plus an editable `variableName` (for example `research_consent`). The assistant can allocate omitted IDs at creation; inspect the result before referencing them. Boolean true means agreement, false means refusal, and absence means unanswered. Compare `{ "slotId": "<config.slotId>", "method": "get" }` with a boolean constant. `isDefined` tests either recorded decision. Never use the literal `accepted` as a universal slot ID.
65
-
66
- To show a section only after agreement, place the agreement expression on the dependent item's or group's root display condition. Hiding retains previously entered answers in the session but omits hidden items from submission and subsequent exports; showing the section again restores them. Required-question navigation enforcement is planned separately.
67
-
68
- ## choiceItem
69
-
70
- Interactive choice question:
71
-
72
- ```json
73
- {
74
- "itemType": "choiceItem",
75
- "config": {
76
- "id": "<itemId>",
77
- "maxSelection": 1,
78
- "shuffleOptions": false,
79
- "options": [
80
- {
81
- "id": "fuji",
82
- "code": "FUJI"
83
- }
84
- ]
85
- }
86
- }
87
- ```
88
-
89
- Choice conventions:
90
-
91
- - `maxSelection: 1` means single select.
92
- - `maxSelection: null` means multiple select with no cap.
93
- - `maxSelection: n` means multiple select capped at n selections.
94
- - If the user says "multiple choice", "multiple answer", "select all that apply", "choose all", or "checkboxes", create a `choiceItem` with `maxSelection: null` unless the user gives a specific cap.
95
- - If the user says "choose up to N", "select at most N", or "maximum N", create a `choiceItem` with `maxSelection: N`.
96
- - If the user says "single choice", "choose one", or options are mutually exclusive by wording, create a `choiceItem` with `maxSelection: 1`.
97
- - `options[].id` is the stable option id.
98
- - `options[].code` is an export code. Responses and expressions continue to use the exact option ID; codes, labels and IDs are independent.
99
- - `options[].hasDisplayCondition` is an optional boolean used by the editor to mark options that participate in branching/display logic.
100
- - Option labels are translations at `options.<optionId>.label`.
101
- - Do not use provider-specific label metadata such as `labelKey`. CASE resolves every respondent-visible option label from `options.<optionId>.label` translations.
102
- - CASE choice options do not currently have a separate visible option-description field. If source material has an option-specific parenthetical/note, include it in the option label or move it to nearby question content. The `survey_change` normalizer preserves shorthand `option.note`, `option.description`, `option.helpText`, and `option.footnoteMarker` by folding them into the label translation.
103
- - If an option has an embedded form, do not duplicate trailing completion words in both the option label and field label. The normalizer strips trailing `, namelijk`, `, please specify`, `, specify`, `, explain`, and `, toelichting` from embedded-form option labels before validation.
104
- - The main response slot is the persistent `config.id`; new assistant-created choices default it to `item.id`. `config.variableName` is its independent coding name. Preserve both IDs on later edits and use structured response references.
105
- - Single select returns `reference`; multiple select returns `reference[]`.
106
-
107
- Embedded option forms:
108
-
109
- - There is no `allowOther` flag in the current choice item model. Do not use `allowOther` to create free-text "Other" inputs.
110
- - CRITICAL — exact key name: the field on the option object is `embeddedForm` (singular, one object). It is NEVER `embeddedForms` (plural) and NEVER an array. The option's recognized keys are only: `id`, `code`, `label`, `hasDisplayCondition`, `embeddedFormItemId`, `embeddedForm`. Any other key (including a typo'd/pluralized `embeddedForms`) is silently dropped during validation — no error is raised, the field simply never appears, and you must not report the change as successful if you are not certain you used the exact singular key.
111
- - CRITICAL — exact shape: `embeddedForm` fields are never listed directly as `embeddedForm.fields`. They must be nested inside `embeddedForm.fieldGroups`, an array of groups, each with its own `fields` array: `{ "id": "...", "fieldGroups": [ { "id": "...", "fields": [ { "id": "...", "type": "input", "controller": { "type": "input", ... } } ] } ] }`. A top-level `fields` array with no `fieldGroups` wrapper will not render anything.
112
- - CRITICAL — `embeddedForm` is a FLAT object with `id`, `authoringMode`, and `fieldGroups`. Do NOT invent an envelope like `{ "itemType": "formItem", "config": { "fields": [...] } }`. There is no `itemType`/`config` wrapper anywhere inside `embeddedForm`; `fieldGroups` sits directly on the `embeddedForm` object itself.
113
- - CRITICAL — always set an explicit `id` on any option that will have an `embeddedForm` (e.g. `"id": "other"`, lowercase, coding-friendly). If you omit `id`, one is auto-generated from `label` and PRESERVES THE EXACT CASE of the label text (e.g. label "Other" auto-generates id "Other", not "other") — you cannot reliably predict it, and every embedded-form translation content key (`embeddedForms.<optionId>...`) must match that exact generated id, including case. Setting an explicit lowercase `id` yourself removes this guesswork entirely.
114
- - To attach input controls to a choice option, put an `embeddedForm` on that option. It uses the same `FormItemConfig` shape as a `formItem` (`id` + `authoringMode` + `fieldGroups`), but it is embedded directly on the option object — it is never a separate `formItem` sibling and never uses `embeddedFormItemId` unless you are intentionally referencing a shared/reusable form item defined elsewhere (rare; prefer inline `embeddedForm`).
115
- - Use `authoringMode: "simple"` only for one internal group containing exactly one field. Use `custom` for a blank or multi-field follow-up. Adding, removing, moving, retyping, or renaming fields, or changing groups/layouts, is a one-way transition to `custom`; preserve the existing field and translations during that transition.
116
- - The embedded form renders only while that option is selected. When the option is deselected, existing embedded responses are cleared after user confirmation.
117
- - Embedded form translation keys are prefixed with `embeddedForms.<optionId>`; for example `embeddedForms.other.fields.other_text.label`. Note this translation content-key prefix IS plural (`embeddedForms`) even though the config field on the option is singular (`embeddedForm`) — these are two different things; do not confuse them.
118
- - Embedded response slots are persistent field IDs. Use their independent survey-wide unique variable names for coding; never reconstruct a slot ID from a translation namespace.
119
- - Use `create-item` with embedded form config and matching translations when creating a new choice item. For an existing choice option, inspect its exact item and option ids and use `set-embedded-form`; its relative form translations are prefixed automatically. If the option is also new, put `add-choice-option` with an explicit id immediately before `set-embedded-form` in the same proposal.
120
- - MANDATORY: every embedded form field needs a matching `embeddedForms.<optionId>.fields.<fieldId>.label` translation entry when it is introduced. There is no fallback/derived label text — a field without this translation renders its raw key to respondents.
121
-
122
- Example "Other, please specify" option:
123
-
124
- ```json
125
- {
126
- "id": "other",
127
- "embeddedForm": {
128
- "id": "other",
129
- "authoringMode": "simple",
130
- "fieldGroups": [
131
- {
132
- "id": "other_details",
133
- "layout": {
134
- "base": 1,
135
- "xs": 1,
136
- "sm": 1,
137
- "md": 1,
138
- "xl": 1
139
- },
140
- "fields": [
141
- {
142
- "id": "other_text",
143
- "type": "input",
144
- "required": true,
145
- "controller": {
146
- "type": "input",
147
- "inputMode": "text"
148
- },
149
- "validations": [],
150
- "variableName": "other_text"
151
- }
152
- ]
153
- }
154
- ]
155
- },
156
- "code": "other"
157
- }
158
- ```
159
-
160
- Matching translation keys on the choice item:
161
-
162
- - `options.other.label`: "Other"
163
- - `embeddedForms.other.fields.other_text.label`: "Please specify"
164
- - Optional placeholder: `embeddedForms.other.fields.other_text.placeholder`
165
- - Additional embedded field content keys follow the same form-field families as a top-level form item, for example:
166
- - `embeddedForms.other.fields.other_text.description`
167
- - `embeddedForms.other.fields.other_text.error`
168
- - `embeddedForms.other.fields.other_text.unit`
169
- - `embeddedForms.other.fields.other_text.checkedLabel`
170
- - `embeddedForms.other.fields.other_text.uncheckedLabel`
171
- - `embeddedForms.other.fields.other_text.options.<optionId>.label`
172
- - `embeddedForms.other.fields.other_text.validations.<validationId>.message`
173
- - `embeddedForms.other.fieldGroups.<groupId>.title`
174
- - `embeddedForms.other.fieldGroups.<groupId>.description`
175
-
176
- ### Full worked example: create a choiceItem with an embedded "Other" text field
177
-
178
- One `create-item` operation carries the whole item plus every translation it needs — the option labels AND the embedded form field label:
179
-
180
- ```json
181
- {
182
- "kind": "create-item",
183
- "target": {
184
- "parentItemId": "<root-group-id>",
185
- "index": 1
186
- },
187
- "item": {
188
- "id": "choice_2",
189
- "itemType": "choiceItem",
190
- "config": {
191
- "id": "choice_2",
192
- "maxSelection": 1,
193
- "shuffleOptions": false,
194
- "options": [
195
- {
196
- "id": "strawberry",
197
- "code": "strawberry"
198
- },
199
- {
200
- "id": "blueberry",
201
- "code": "blueberry"
202
- },
203
- {
204
- "id": "other",
205
- "embeddedForm": {
206
- "id": "other",
207
- "authoringMode": "simple",
208
- "fieldGroups": [
209
- {
210
- "id": "other_details",
211
- "layout": {
212
- "base": 1,
213
- "xs": 1,
214
- "sm": 1,
215
- "md": 1,
216
- "xl": 1
217
- },
218
- "fields": [
219
- {
220
- "id": "other_text",
221
- "type": "input",
222
- "required": true,
223
- "controller": {
224
- "type": "input",
225
- "inputMode": "text"
226
- },
227
- "validations": [],
228
- "variableName": "other_text"
229
- }
230
- ]
231
- }
232
- ]
233
- },
234
- "code": "other"
235
- }
236
- ],
237
- "variableName": "choice_2"
238
- }
239
- },
240
- "translations": [
241
- {
242
- "locale": "en",
243
- "contentKey": "title",
244
- "plainText": "Favorite berry"
245
- },
246
- {
247
- "locale": "en",
248
- "contentKey": "options.strawberry.label",
249
- "plainText": "Strawberry"
250
- },
251
- {
252
- "locale": "en",
253
- "contentKey": "options.blueberry.label",
254
- "plainText": "Blueberry"
255
- },
256
- {
257
- "locale": "en",
258
- "contentKey": "options.other.label",
259
- "plainText": "Other"
260
- },
261
- {
262
- "locale": "en",
263
- "contentKey": "embeddedForms.other.fields.other_text.label",
264
- "plainText": "Please specify"
265
- }
266
- ]
267
- }
268
- ```
269
-
270
- Notice `config.id` equals the item id, every option has a label translation, and the embedded field has its own label translation under the `embeddedForms.other.*` prefix — omitting any one of these leaves that piece of UI blank.
271
-
272
- ### Full worked example: create a multiple choice question
273
-
274
- Use `maxSelection: null` for "select all that apply" / checkbox-style questions:
275
-
276
- ```json
277
- {
278
- "kind": "create-item",
279
- "target": {
280
- "parentItemId": "<root-group-id>",
281
- "index": 1
282
- },
283
- "item": {
284
- "id": "industries_worked",
285
- "itemType": "choiceItem",
286
- "config": {
287
- "id": "industries_worked",
288
- "maxSelection": null,
289
- "shuffleOptions": false,
290
- "options": [
291
- {
292
- "id": "healthcare",
293
- "code": "HEALTHCARE"
294
- },
295
- {
296
- "id": "education",
297
- "code": "EDUCATION"
298
- },
299
- {
300
- "id": "food_service",
301
- "code": "FOOD_SERVICE"
302
- }
303
- ],
304
- "variableName": "industries_worked"
305
- }
306
- },
307
- "translations": [
308
- {
309
- "locale": "en",
310
- "contentKey": "title",
311
- "plainText": "Which industries have you worked in? Select all that apply."
312
- },
313
- {
314
- "locale": "en",
315
- "contentKey": "options.healthcare.label",
316
- "plainText": "Healthcare"
317
- },
318
- {
319
- "locale": "en",
320
- "contentKey": "options.education.label",
321
- "plainText": "Education"
322
- },
323
- {
324
- "locale": "en",
325
- "contentKey": "options.food_service.label",
326
- "plainText": "Food service"
327
- }
328
- ]
329
- }
330
- ```
331
-
332
- ## formItem
333
-
334
- Interactive form question:
335
-
336
- ```json
337
- {
338
- "itemType": "formItem",
339
- "config": {
340
- "id": "<itemId>",
341
- "authoringMode": "custom",
342
- "fieldGroups": [
343
- {
344
- "id": "main",
345
- "layout": {
346
- "base": 1,
347
- "xs": 1,
348
- "sm": 1,
349
- "md": 1,
350
- "xl": 1
351
- },
352
- "fields": []
353
- }
354
- ]
355
- }
356
- }
357
- ```
358
-
359
- The item's config has `id`, `authoringMode`, and `fieldGroups`. `fieldGroups` sits directly on config — never nest it under another wrapper, and never put a top-level `fields` array directly on config.
360
-
361
- Authoring modes and Add item presets:
362
-
363
- - `Text` → `input`, `Long text` → `textarea`, `Number` → `number`, `Slider` → `slider`, `Dropdown` → `select`, `Date` → `datepicker`, and `Switch` → `switch`.
364
- - These seven presets use `authoringMode: "simple"` with exactly one internal field group containing exactly one field. The field group remains part of persisted/runtime data; hiding it is only an editor abstraction.
365
- - `Blank form` uses `authoringMode: "custom"`. Any finished multi-field/group form also uses custom mode.
366
- - Simple mode exposes the existing field's label, description, required state, and compatible controller settings. It does not expose groups, layouts, field reordering, field variable names, or field-type switching.
367
- - Adding/removing/moving fields or groups, changing layout, or changing the sole field's variableName/type is a one-way transition to custom. Preserve the existing field and every translation. Never leave a multi-field, empty, or multi-group structure marked simple.
368
- - If a stored simple form lacks its sole first field, treat it as custom without deleting any remaining data.
369
-
370
- Fields normally need at least one non-empty group to be useful. An explicit `authoringMode: "custom"` with one empty group is the supported Blank form starting preset.
371
-
372
- Field group config:
373
-
374
- - `id`: stable group id.
375
- - `hasDisplayCondition`: optional boolean used by the editor to mark groups that participate in branching/display logic.
376
- - `layout`: column counts by container breakpoint, using integers 1 through 4. These are grid column counts, not field spans; use 1 at every breakpoint for a lone full-width field.
377
-
378
- Example field group layout:
379
-
380
- ```json
381
- {
382
- "id": "main",
383
- "hasDisplayCondition": true,
384
- "layout": {
385
- "base": 1,
386
- "xs": 1,
387
- "sm": 2,
388
- "md": 2,
389
- "xl": 3
390
- },
391
- "fields": []
392
- }
393
- ```
394
-
395
- Field group translations:
396
-
397
- - `fieldGroups.<groupId>.title`
398
- - `fieldGroups.<groupId>.description`
399
-
400
- Field shape:
401
-
402
- ```json
403
- {
404
- "id": "age",
405
- "type": "number",
406
- "required": true,
407
- "controller": {
408
- "type": "number",
409
- "step": 1
410
- },
411
- "validations": [],
412
- "variableName": "age"
413
- }
414
- ```
415
-
416
- `id` is the persistent response-slot ID, also used in field translation keys. `variableName` is its independent survey-wide unique coding name. Name each field for its meaning and units, including embedded fields. Renaming preserves slot IDs, answers and expressions. Do not reuse generic variable names across unrelated items.
417
-
418
- Field types and default controllers:
419
-
420
- - `input`: `{ "type": "input", "inputMode": "text" }`
421
- - `textarea`: `{ "type": "textarea", "rows": 3 }`
422
- - `number`: `{ "type": "number", "step": 1 }`
423
- - `datepicker`: `{ "type": "datepicker", "mode": "year-month-day" }`
424
- - `switch`: `{ "type": "switch", "defaultChecked": false }`
425
- - `select`: `{ "type": "select", "options": [] }`
426
- - `slider`: `{ "type": "slider", "min": 0, "max": 100, "step": 1, "defaultValue": 50, "showValue": true }`
427
-
428
- Supported controller properties by type:
429
-
430
- - `input`: `placeholder`, `inputMode`, `autocomplete`
431
- - `textarea`: `placeholder`, `rows`
432
- - `number`: `placeholder`, `step`, `unit`, `defaultValue`
433
- - `datepicker`: `placeholder`, `mode` where mode is `year`, `year-month`, or `year-month-day`
434
- - `switch`: `checkedLabel`, `uncheckedLabel`, `defaultChecked`
435
- - `select`: `placeholder`, `allowClear`, `options`
436
- - `slider`: `min`, `max`, `step`, `defaultValue`, `showValue`
437
-
438
- Select controller option shape:
439
-
440
- ```json
441
- {
442
- "id": "weekly",
443
- "code": "weekly"
444
- }
445
- ```
446
-
447
- Visible select-option labels do NOT live in the controller config. They live in translations at `fields.<fieldId>.options.<optionId>.label`.
448
- If a select controller option has `id: "minder_7_dagen", code: "minder_7_dagen"`, the visible Dutch label still needs a separate translation such as `fields.<fieldId>.options.minder_7_dagen.label`: "minder dan 7 dagen". Without this translation, respondents see the raw identifier/code in the dropdown.
449
-
450
- Field translation keys:
451
-
452
- - `fields.<fieldId>.label`
453
- - `fields.<fieldId>.description`
454
- - `fields.<fieldId>.error`
455
- - `fields.<fieldId>.placeholder`
456
- - `fields.<fieldId>.unit`
457
- - `fields.<fieldId>.checkedLabel`
458
- - `fields.<fieldId>.uncheckedLabel`
459
- - `fields.<fieldId>.options.<optionId>.label`
460
- - `fields.<fieldId>.validations.<validationId>.message`
461
-
462
- Validation configs use `{ "id", "type", "value"?, "message"? }`.
463
- Supported validation types: `required`, `minLength`, `maxLength`, `pattern`, `email`, `url`, `integer`, `min`, `max`, `minDate`, `maxDate`, `mustBeTrue`.
464
-
465
- MANDATORY: every field needs a matching `fields.<fieldId>.label` translation entry in the SAME operation that creates it. There is no fallback/derived label text — a field without this translation renders with no visible label (only its raw type, e.g. "Input") to respondents. Before returning a create-item or raw-patch result, check that you added exactly one label translation per field you defined.
466
- MANDATORY: every `select` option needs a matching `fields.<fieldId>.options.<optionId>.label` translation entry in the SAME operation that creates it. Do not rely on `controller.options[].label`; generated survey JSON strips option labels from config and restores them from translations. Missing select option translations render raw values such as `minder_7_dagen`.
467
-
468
- ### Full worked example: create a formItem with two fields
469
-
470
- ```json
471
- {
472
- "kind": "create-item",
473
- "target": {
474
- "parentItemId": "<root-group-id>",
475
- "index": 1
476
- },
477
- "item": {
478
- "id": "form_1",
479
- "itemType": "formItem",
480
- "config": {
481
- "id": "form_1",
482
- "authoringMode": "custom",
483
- "fieldGroups": [
484
- {
485
- "id": "main",
486
- "layout": {
487
- "base": 1,
488
- "xs": 1,
489
- "sm": 1,
490
- "md": 1,
491
- "xl": 1
492
- },
493
- "fields": [
494
- {
495
- "id": "age",
496
- "type": "number",
497
- "required": true,
498
- "controller": {
499
- "type": "number",
500
- "step": 1
501
- },
502
- "validations": [],
503
- "variableName": "age"
504
- },
505
- {
506
- "id": "comments",
507
- "type": "textarea",
508
- "controller": {
509
- "type": "textarea",
510
- "rows": 3
511
- },
512
- "validations": [],
513
- "variableName": "comments"
514
- }
515
- ]
516
- }
517
- ]
518
- }
519
- },
520
- "translations": [
521
- {
522
- "locale": "en",
523
- "contentKey": "title",
524
- "plainText": "About you"
525
- },
526
- {
527
- "locale": "en",
528
- "contentKey": "fields.age.label",
529
- "plainText": "Your age"
530
- },
531
- {
532
- "locale": "en",
533
- "contentKey": "fields.comments.label",
534
- "plainText": "Any comments?"
535
- }
536
- ]
537
- }
538
- ```
539
-
540
- ## choiceMatrix
541
-
542
- Toolbox presets have exact server-supported IDs on `create-item.presetId`:
543
-
544
- | User wording | presetId | Actual toolbox defaults |
545
- | ---------------------- | -------------------------- | ---------------------------------------------------- |
546
- | Yes / No matrix | `choiceMatrix.yesNo` | Yes, No; scores 1, 2 |
547
- | 5-point scale / Likert | `choiceMatrix.fivePoint` | Strongly disagree through Strongly agree; scores 1–5 |
548
- | 7-point scale / Likert | `choiceMatrix.sevenPoint` | Seven agreement options; scores 1–7 |
549
- | 5-point bipolar scale | `choiceMatrix.bipolarFive` | Five numbered options, scores 1–5, `bipolar: true` |
550
-
551
- These are editable starting configurations, not fixed measurement instruments. The bipolar preset does NOT imply scores −2…2: request those scores explicitly when wanted. Preset option labels are localized by the shared toolbox factory (English, Dutch, German). Supply `item.config.rows` and translations for `title` and every row label. Optional config values override preset defaults. Set an explicit primitive `config.id` if adding `<config.id>.startLabel`/`endLabel` at creation. The validated result exposes the generated option IDs for subsequent edits; do not guess them. With no `presetId`, author the complete custom scale. Existing toolbox items are ordinary matrices and all their settings remain editable.
552
-
553
- Match the response wording to the question's construct: agreement labels answer statements, while liking, satisfaction, frequency and intensity need corresponding anchors. Preserve exact toolbox wording when the user explicitly requests those defaults. When custom anchors are already known, include the complete ordered `item.config.options` with explicit stable IDs, scores and matching `<optionId>.label` translations in the creation operation; these replace preset options. This creates the intended scale directly without a second pass over generated IDs. For later edits, copy the existing IDs exactly and preserve them.
554
-
555
- Use a choice matrix for several row statements sharing the same categorical scale. Persistent IDs identify the primitive, rows, options, per-row computed scores and total. `prefix` and each `rowName` derive survey-wide unique names: `<prefix>_<rowName>`, `<prefix>_<rowName>_score` and `<prefix>_sum`. Inspect declared slots; do not synthesize IDs from those names.
556
-
557
- ```json
558
- {
559
- "id": "wellbeing_matrix",
560
- "itemType": "choiceMatrix",
561
- "config": {
562
- "id": "wellbeing_primitive",
563
- "prefix": "wellbeing",
564
- "sumSlotId": "wellbeing_total",
565
- "rows": [{ "id": "sleep_answer", "scoreSlotId": "sleep_score", "rowName": "sleep" }],
566
- "options": [
567
- { "id": "never", "code": "never", "score": 0 },
568
- { "id": "always", "code": "always", "score": 4 }
569
- ],
570
- "layout": "automatic"
571
- }
572
- }
573
- ```
574
-
575
- Create with translations for `title`, `sleep_answer.label`, `never.label` and `always.label`. Option scores are numeric scoring semantics; codes are export strings and labels are translated wording. Supply all scores or leave them all absent. Optional `leadingNonScoring`/`trailingNonScoring` each hold ONE `{id, code?}` object (never an array and never a score). Each participates in answers but not scoring. There is at most one leading and one trailing non-scoring choice; a second at the same end requires an explicit product decision, not silently overwriting the first. Optional bipolar endpoints use `<config.id>.startLabel`/`endLabel`. Layout accepts `automatic`, `stacked` or `matrix`; `rowLabelPosition` accepts `beside` or `above`.
576
-
577
- Stored row answers are `reference` values containing exact option IDs. Computed score and sum slots are read-only and excluded from default exports unless explicitly included. A non-scoring or unanswered row has no score; a numeric score of zero is a defined score. The sum is absent when no visible rows have scored answers. Required rows use `required: true`. Use config patches for structural row/option/layout edits, translations for visible text, and `update-response-settings` for naming components, export codes and formats. Preserve surviving IDs. Reinspect expression diagnostics after removing slots/options.
578
-
579
- ### Editing a choice matrix
580
-
581
- Inspect the current config before choosing array indices. Put all related patches and new translations in one `patch-item-config` operation. Example adding a final “Don't know” answer:
582
-
583
- ```json
584
- {
585
- "kind": "patch-item-config",
586
- "itemId": "<inspected item ID>",
587
- "patches": [
588
- { "op": "add", "path": "/trailingNonScoring", "value": { "id": "dont_know", "code": "DK" } }
589
- ],
590
- "translations": [{ "locale": "en", "contentKey": "dont_know.label", "plainText": "Don't know" }]
591
- }
592
- ```
593
-
594
- - Add a row at `/rows/-` with fresh `id`, `scoreSlotId`, unique `rowName` and its `<id>.label` translation. Duplicate by copying the row settings and all relevant locales into NEW IDs. Keep surviving IDs unchanged.
595
- - Remove rows/options with `remove` at the inspected index; reorder with `move` or replace the complete inspected array without changing IDs. `copy` alone duplicates IDs and is invalid. Remove or repair dependent expressions in the same turn.
596
- - Add scale options at `/options/-` with `id`, optional `code`, and a score if the existing scale is scored. Include `<optionId>.label`. To disable scoring, remove every scale score in one operation. To reverse scoring, reverse score VALUES in the existing options; do not reorder options or labels. For zero-based/one-based/bipolar scoring, assign 0…n−1 / 1…n / −(n−1)/2…(n−1)/2 to all scale options in one patch. Non-scoring choices stay outside this array.
597
- - Add or remove `/leadingNonScoring` or `/trailingNonScoring` as object properties. To move a non-scoring option from one end to the other, preserve its ID and label. To switch an option between scale and non-scoring roles, move the same ID atomically and add/remove its score appropriately.
598
- - Set `/bipolar` true and translate `<config.id>.startLabel` and `<config.id>.endLabel` for endpoints. Set `/rowLabelPosition` to `beside` or `above`. Layout and positive `stackingBreakpoint` affect presentation, not responses.
599
- - Row display conditions use `survey_expression` mutation `set-component-display-condition` with the exact row ID; required-row messages use `<rowId>.message`. Whole-question visibility uses `set-item-display-condition`. The current matrix player supports row visibility, not individual option/column/cell visibility or component disabling; do not claim those effects from merely storing an expression.
600
-
601
- ## formMatrix
602
-
603
- Use a form matrix for independent row/column cells, with a column-level field type and optional cell overrides. Names derive from `<prefix>_<rowName>_<columnName>`; IDs and existing answers survive naming, ordering and layout edits. Every row/column intersection needs a persistent cell object, including static or empty cells, although only interactive cells declare response slots.
604
-
605
- ```json
606
- {
607
- "id": "visits_matrix",
608
- "itemType": "formMatrix",
609
- "config": {
610
- "id": "visits_primitive",
611
- "prefix": "visits",
612
- "rows": [{ "id": "visit_row", "rowName": "baseline" }],
613
- "columns": [
614
- {
615
- "id": "weight_column",
616
- "columnName": "weight_kg",
617
- "field": { "type": "number", "unit": "kg", "validations": [] }
618
- }
619
- ],
620
- "cells": [{ "id": "baseline_weight", "rowId": "visit_row", "columnId": "weight_column" }],
621
- "layout": "automatic"
622
- }
623
- }
624
- ```
625
-
626
- Translate `title`, `<rowId>.label`, `<columnId>.label`, and cell-specific labels/help when needed. Field types are `text` (optional multiline), `number` (step/unit), `date` (mode `year`, `year-month`, `year-month-day`), `dropdown` (options `[{id,code}]`), `boolean`, `static`, `empty`. Interactive field definitions carry `validations: []` and optional `required`. Dropdown answers contain option IDs; visible option labels use `<optionId>.label`. Cell overrides use the same field schema. Static cells use `<cellId>.content`. Use the capability's declared content surfaces for localization.
627
-
628
- ### Editing a form matrix
629
-
630
- Config patches handle structural cells, overrides, columns and rows. Every structural edit must produce a complete rectangle in ONE `patch-item-config` operation:
631
-
632
- - For multi-row/column duplication, inspect the current arrays first, then construct the complete desired rows, columns and cells while retaining all surviving objects and IDs. Replacing these arrays together can be clearer than a long chain of index-sensitive patches. A duplicate is an addition; never rename or repurpose its source. Keep existing coding unchanged; choose short coding components for new units.
633
- - Adding a row also adds one fresh `{id,rowId,columnId}` cell per existing column; adding a column adds a cell for every row. Removing a row/column removes its associated cells in the same patch. The executor automatically cleans translations owned by removed components in every locale and inactive cell content after resetting overrides; inspect the result before attempting any extra raw translation removals. Reordering rows/columns changes only array order; preserve cell IDs and coordinates.
634
- - A column's `field` defines inherited settings. A cell `override` is a COMPLETE field definition, not a partial patch; use `add` at `/cells/<index>/override` when absent, or replace it when already present. Its content belongs to the cell ID. Reset it with `remove` to inherit the column again. Static and empty cells remain in `cells`, but have no response slot. Retyping a cell or column retains cell IDs, and may invalidate answers or expressions; inspect diagnostics and repair dependent logic when needed.
635
- - Shared field content belongs to `<columnId>.<role>`; overridden content belongs to `<cellId>.<role>`. Column headers always use `<columnId>.label`. Roles are `label`, `description`, `placeholder`, `message`, `content`, `yesLabel`, `noLabel`. Static text belongs to `<columnId>.content` when inherited or `<cellId>.content` when overridden. Number `unit` is a config string, not a content key.
636
- - Dropdown fields use `options: [{id,code}]`, with globally distinct component IDs and `<optionId>.label` translations. For an independent cell override copied from a shared dropdown, allocate new option and validation IDs and copy their translations in all existing locales. Reusing column-owned option/rule IDs in an override is invalid. Duplicate rows/columns the same way: new unit and cell IDs; deep-copy every existing override with new option/rule IDs and translated content. An override on the source must remain an override on the duplicate; omitting it silently changes that cell to the column default. After duplication, compare source and destination effective field types, settings, option codes and translated content, allowing only explicitly requested differences.
637
- - Interactive fields require `validations: []` even when empty. Required is the field's boolean `required` property. Text validations: `minLength`, `maxLength`, `pattern`; number: `integer`, `min`, `max`; date: `minDate`, `maxDate`; boolean: `mustBeTrue`; dropdown has no additional rules. Each rule has a fresh `id`, `type`, and a `value` where needed. Date values are calendar strings matching precision, e.g. `2026-09`, not timestamps. Date expressions go in `valueExpression` and use the expression grammar. Rule messages are `<ruleId>.message`; field required messages are `<fieldOwnerId>.message`. Bounds must be compatible, and minimum cannot exceed maximum.
638
- - Column `width` and `rowLabelWidth` accept `{mode:"default"}`, `{mode:"fixed",pixels:160}`, or `{mode:"fit",minPixels:120}` (numeric minima 48). Layout is `automatic`, `stacked`, or `matrix`; `stackingBreakpoint` is a positive pixel width. Boolean arrangement is `horizontal` or `vertical`; text uses `multiline`; dates use `mode`; numbers use `step` and `unit`.
639
- - Put new visible translations inside the patch operation; blank or incorrect namespaces are rejected. For row visibility, use `survey_expression` mutation `set-component-display-condition` with the row ID. Whole-question visibility uses `set-item-display-condition`. The matrix player currently supports row visibility, not column/cell visibility or component disabling. Hidden-row answers are retained in the session and restored when shown, but excluded from expressions, validation, scoring and submission while hidden.
640
-
641
- Coding uses one matrix naming edit with `primitiveId`, `prefix`, `rows` and/or `columns`; never set arbitrary names or headers on derived cells. A shared dropdown code change affects all cells using that option set. Keep numeric scores, identities and translated wording separate from coding changes.