@case-framework/survey-assistant 0.1.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 (98) hide show
  1. package/README.md +212 -0
  2. package/dist/authoring-references-Cy8J-DWt.d.mts +48 -0
  3. package/dist/authoring-references-Cy8J-DWt.d.mts.map +1 -0
  4. package/dist/authoring-references-X6uRaWuf.mjs +68 -0
  5. package/dist/authoring-references-X6uRaWuf.mjs.map +1 -0
  6. package/dist/capabilities-B6tvE7fK.mjs +66 -0
  7. package/dist/capabilities-B6tvE7fK.mjs.map +1 -0
  8. package/dist/capabilities-DvSKtjAw.d.mts +182 -0
  9. package/dist/capabilities-DvSKtjAw.d.mts.map +1 -0
  10. package/dist/capabilities-default.d.mts +15 -0
  11. package/dist/capabilities-default.d.mts.map +1 -0
  12. package/dist/capabilities-default.mjs +347 -0
  13. package/dist/capabilities-default.mjs.map +1 -0
  14. package/dist/constants-CFUWrRtl.mjs +7 -0
  15. package/dist/constants-CFUWrRtl.mjs.map +1 -0
  16. package/dist/constants-Jw0k8ojS.d.mts +67 -0
  17. package/dist/constants-Jw0k8ojS.d.mts.map +1 -0
  18. package/dist/controller-proxy-BRDusFiO.d.mts +57 -0
  19. package/dist/controller-proxy-BRDusFiO.d.mts.map +1 -0
  20. package/dist/controller-proxy-Duxo2IFE.mjs +92 -0
  21. package/dist/controller-proxy-Duxo2IFE.mjs.map +1 -0
  22. package/dist/digest-BsoVq49o.d.mts +9 -0
  23. package/dist/digest-BsoVq49o.d.mts.map +1 -0
  24. package/dist/digest.d.mts +2 -0
  25. package/dist/digest.mjs +20 -0
  26. package/dist/digest.mjs.map +1 -0
  27. package/dist/engine-sPvuixWg.mjs +4857 -0
  28. package/dist/engine-sPvuixWg.mjs.map +1 -0
  29. package/dist/engine.d.mts +3 -0
  30. package/dist/engine.mjs +4 -0
  31. package/dist/index-BWNHkpkh.d.mts +187 -0
  32. package/dist/index-BWNHkpkh.d.mts.map +1 -0
  33. package/dist/index-CUqbBjkc.d.mts +641 -0
  34. package/dist/index-CUqbBjkc.d.mts.map +1 -0
  35. package/dist/index-MpEMQzTm.d.mts +123 -0
  36. package/dist/index-MpEMQzTm.d.mts.map +1 -0
  37. package/dist/index-TFtvBiSQ.d.mts +1149 -0
  38. package/dist/index-TFtvBiSQ.d.mts.map +1 -0
  39. package/dist/memory-thread-repository-CJkkVE2-.mjs +718 -0
  40. package/dist/memory-thread-repository-CJkkVE2-.mjs.map +1 -0
  41. package/dist/protocol-DV0ka9WT.mjs +1032 -0
  42. package/dist/protocol-DV0ka9WT.mjs.map +1 -0
  43. package/dist/protocol.d.mts +3 -0
  44. package/dist/protocol.mjs +2 -0
  45. package/dist/react-VuYe-cPh.mjs +521 -0
  46. package/dist/react-VuYe-cPh.mjs.map +1 -0
  47. package/dist/react-integration.d.mts +2 -0
  48. package/dist/react-integration.mjs +3 -0
  49. package/dist/react.d.mts +3 -0
  50. package/dist/react.mjs +4 -0
  51. package/dist/references/assistant-operations.md +287 -0
  52. package/dist/references/core-rules.md +11 -0
  53. package/dist/references/embedded-forms.md +80 -0
  54. package/dist/references/expressions.md +301 -0
  55. package/dist/references/item-types.md +364 -0
  56. package/dist/references/localization.md +25 -0
  57. package/dist/references/rich-text-content.md +193 -0
  58. package/dist/references/source-material-surveys.md +60 -0
  59. package/dist/references/survey-data-model.md +109 -0
  60. package/dist/references/survey-quality.md +23 -0
  61. package/dist/request-context-jcmXT_Q2.mjs +44 -0
  62. package/dist/request-context-jcmXT_Q2.mjs.map +1 -0
  63. package/dist/request-guardrails-C59IWnH8.mjs +73 -0
  64. package/dist/request-guardrails-C59IWnH8.mjs.map +1 -0
  65. package/dist/server-agent.d.mts +131 -0
  66. package/dist/server-agent.d.mts.map +1 -0
  67. package/dist/server-agent.mjs +229 -0
  68. package/dist/server-agent.mjs.map +1 -0
  69. package/dist/server-runtime.d.mts +123 -0
  70. package/dist/server-runtime.d.mts.map +1 -0
  71. package/dist/server-runtime.mjs +296 -0
  72. package/dist/server-runtime.mjs.map +1 -0
  73. package/dist/server-tasks.d.mts +78 -0
  74. package/dist/server-tasks.d.mts.map +1 -0
  75. package/dist/server-tasks.mjs +253 -0
  76. package/dist/server-tasks.mjs.map +1 -0
  77. package/dist/server-tools.d.mts +2 -0
  78. package/dist/server-tools.mjs +2 -0
  79. package/dist/server.d.mts +52 -0
  80. package/dist/server.d.mts.map +1 -0
  81. package/dist/server.mjs +6 -0
  82. package/dist/storage-postgres.d.mts +37 -0
  83. package/dist/storage-postgres.d.mts.map +1 -0
  84. package/dist/storage-postgres.mjs +167 -0
  85. package/dist/storage-postgres.mjs.map +1 -0
  86. package/dist/thread-documents-DamIq9Od.d.mts +91 -0
  87. package/dist/thread-documents-DamIq9Od.d.mts.map +1 -0
  88. package/dist/thread-handlers-DpynUc64.d.mts +112 -0
  89. package/dist/thread-handlers-DpynUc64.d.mts.map +1 -0
  90. package/dist/tools-Hz2p7vyv.mjs +1879 -0
  91. package/dist/tools-Hz2p7vyv.mjs.map +1 -0
  92. package/dist/ui.css +2 -0
  93. package/dist/ui.d.mts +63 -0
  94. package/dist/ui.d.mts.map +1 -0
  95. package/dist/ui.mjs +2238 -0
  96. package/dist/ui.mjs.map +1 -0
  97. package/package.json +166 -0
  98. package/ui.css.d.ts +3 -0
@@ -0,0 +1,301 @@
1
+ # Expressions, Conditions, Validations, Prefills
2
+
3
+ Use `survey_expression` for expression work. It is the expression capability bundle:
4
+ - `action: "context"` returns available response refs and optional existing expression locations.
5
+ - `action: "prepare"` validates expression refs/return types and registers a candidate change set.
6
+ - Prefer this tool over hand-written raw JSON Patch for expression-bearing locations.
7
+
8
+ ## JsonExpression
9
+
10
+ Expression JSON has four node types:
11
+
12
+ ```json
13
+ { "type": "const", "value": { "type": "boolean", "value": true } }
14
+ { "type": "responseVariable", "variableRef": "<itemId>...get...<slotId>" }
15
+ { "type": "contextVariable", "contextType": "locale" }
16
+ { "type": "function", "functionName": "and", "arguments": [] }
17
+ ```
18
+
19
+ Optional `editorConfig` can contain `usedTemplate`, `label`, and `description`.
20
+
21
+ ## Response Values And Variables
22
+
23
+ Response values are typed objects:
24
+ - string: `{ "type": "string", "value": "abc" }`
25
+ - number: `{ "type": "number", "value": 42 }`
26
+ - boolean: `{ "type": "boolean", "value": true }`
27
+ - date: `{ "type": "date", "value": 1767225600000 }`
28
+ - duration: `{ "type": "duration", "value": 3, "unit": "days" }`
29
+ - reference: `{ "type": "reference", "value": "<optionId>" }`
30
+ - arrays use `string[]`, `number[]`, `date[]`, `duration[]`, `reference[]`.
31
+
32
+ Response variable refs use this exact string format:
33
+ `<itemId>...<method>...<slotId>`
34
+
35
+ Methods:
36
+ - `get`: returns the current slot value.
37
+ - `isDefined`: returns boolean true when a response exists.
38
+
39
+ Never invent response refs. Use the current context response-slots scope or item details.
40
+ The live snapshot is immutable for the whole assistant response. `survey_change` validates and
41
+ registers a candidate; it does not change what later tools can inspect. The browser applies authorized change sets
42
+ only after the response completes successfully. A validated candidate is intended for apply if the turn succeeds;
43
+ it is not a dry run. For a
44
+ request that creates dependent items, plan the complete structure first, put the source create
45
+ operation before its dependents, and embed each dependent expression directly in its
46
+ `create-item.item` object in one `survey_change` call. Do not create the items with
47
+ `survey_change` and then call `survey_expression`: live inspection cannot see unapplied items
48
+ during the same turn.
49
+
50
+ Each response slot defined by a source item's config is available as
51
+ `<sourceItemId>...get...<slotId>`, with a boolean presence check at
52
+ `<sourceItemId>...isDefined...<slotId>`. Use the slot's actual value type to choose the
53
+ function and constant. For example, a single-choice `reference` slot uses `eq` with a
54
+ `reference` const; this is only one typed case, not a restriction on source item types.
55
+ Always check the response slot value type before choosing a function:
56
+ - Single-choice choice items return `reference`; compare them with `eq` and a `reference` const.
57
+ - Multiple-choice choice items return `reference[]`; check selected options with `list_contains` and a `reference` const.
58
+ - `list_contains` must receive `string[]` + `string` or `reference[]` + `reference`. Never use it with a single `reference` response.
59
+
60
+ Context variable types are `locale`, `participantFlag`, `customValue`, and `customExpression`.
61
+
62
+ ## Functions
63
+
64
+ Boolean: `and`, `or`, `not`.
65
+ List containment: `list_contains`.
66
+ Equality/comparison: `eq`, `gt`, `gte`, `lt`, `lte`, `in_range`.
67
+ Numeric aggregation: `sum`, `min`, `max`.
68
+ String equality: `str_eq`.
69
+ Date equality: `date_eq`.
70
+
71
+ Example: show an item when option "yes" is selected on choice item q1:
72
+
73
+ ```json
74
+ {
75
+ "type": "function",
76
+ "functionName": "eq",
77
+ "arguments": [
78
+ { "type": "responseVariable", "variableRef": "q1...get...q1" },
79
+ { "type": "const", "value": { "type": "reference", "value": "yes" } }
80
+ ]
81
+ }
82
+ ```
83
+
84
+ Example: multiple-select contains option "apple" when the inspected response slot type is `reference[]`:
85
+
86
+ ```json
87
+ {
88
+ "type": "function",
89
+ "functionName": "list_contains",
90
+ "arguments": [
91
+ { "type": "responseVariable", "variableRef": "q2...get...q2" },
92
+ { "type": "const", "value": { "type": "reference", "value": "apple" } }
93
+ ]
94
+ }
95
+ ```
96
+
97
+ ## New Items With Dependencies
98
+
99
+ For a newly created source and dependent, use this operation order and expression placement.
100
+ This pattern is item-type independent: `displayConditions` is a sibling of `config` on the
101
+ raw item, never inside `config` or `translations`.
102
+
103
+ ```json
104
+ [
105
+ {
106
+ "kind": "create-item",
107
+ "target": { "parentItemId": "<parent-group-id-or-key>" },
108
+ "item": {
109
+ "id": "routing_choice",
110
+ "key": "routing_choice",
111
+ "itemType": "choiceItem",
112
+ "config": {
113
+ "id": "routing_choice",
114
+ "maxSelection": 1,
115
+ "shuffleOptions": false,
116
+ "options": [
117
+ { "id": "option_a", "key": "option_a" },
118
+ { "id": "option_b", "key": "option_b" }
119
+ ]
120
+ }
121
+ },
122
+ "translations": [
123
+ { "locale": "en", "contentKey": "title", "plainText": "Choose an option" },
124
+ { "locale": "en", "contentKey": "options.option_a.label", "plainText": "Option A" },
125
+ { "locale": "en", "contentKey": "options.option_b.label", "plainText": "Option B" }
126
+ ]
127
+ },
128
+ {
129
+ "kind": "create-item",
130
+ "target": { "parentItemId": "<parent-group-id-or-key>" },
131
+ "item": {
132
+ "id": "follow_up_a",
133
+ "key": "follow_up_a",
134
+ "itemType": "choiceItem",
135
+ "config": {
136
+ "id": "follow_up_a",
137
+ "maxSelection": 1,
138
+ "shuffleOptions": false,
139
+ "options": [
140
+ { "id": "yes", "key": "yes" },
141
+ { "id": "no", "key": "no" }
142
+ ]
143
+ },
144
+ "displayConditions": {
145
+ "root": {
146
+ "type": "function",
147
+ "functionName": "eq",
148
+ "arguments": [
149
+ { "type": "responseVariable", "variableRef": "routing_choice...get...routing_choice" },
150
+ { "type": "const", "value": { "type": "reference", "value": "option_a" } }
151
+ ]
152
+ }
153
+ }
154
+ },
155
+ "translations": [
156
+ { "locale": "en", "contentKey": "title", "plainText": "Follow-up for option A" },
157
+ { "locale": "en", "contentKey": "options.yes.label", "plainText": "Yes" },
158
+ { "locale": "en", "contentKey": "options.no.label", "plainText": "No" }
159
+ ]
160
+ },
161
+ {
162
+ "kind": "create-item",
163
+ "target": { "parentItemId": "<parent-group-id-or-key>" },
164
+ "item": {
165
+ "id": "follow_up_b",
166
+ "key": "follow_up_b",
167
+ "itemType": "choiceItem",
168
+ "config": {
169
+ "id": "follow_up_b",
170
+ "maxSelection": 1,
171
+ "shuffleOptions": false,
172
+ "options": [
173
+ { "id": "yes", "key": "yes" },
174
+ { "id": "no", "key": "no" }
175
+ ]
176
+ },
177
+ "displayConditions": {
178
+ "root": {
179
+ "type": "function",
180
+ "functionName": "eq",
181
+ "arguments": [
182
+ { "type": "responseVariable", "variableRef": "routing_choice...get...routing_choice" },
183
+ { "type": "const", "value": { "type": "reference", "value": "option_b" } }
184
+ ]
185
+ }
186
+ }
187
+ },
188
+ "translations": [
189
+ { "locale": "en", "contentKey": "title", "plainText": "Follow-up for option B" },
190
+ { "locale": "en", "contentKey": "options.yes.label", "plainText": "Yes" },
191
+ { "locale": "en", "contentKey": "options.no.label", "plainText": "No" }
192
+ ]
193
+ }
194
+ ]
195
+ ```
196
+
197
+ Use the same placement on other interactive item types; only their `itemType`, `config`, and
198
+ translations change. The validator makes response slots from earlier creates available to later
199
+ creates in this one operations array.
200
+
201
+ ## survey_expression Mutations
202
+
203
+ Item visibility:
204
+
205
+ ```json
206
+ {
207
+ "kind": "set-item-display-condition",
208
+ "itemId": "followup_yes",
209
+ "expression": {
210
+ "type": "function",
211
+ "functionName": "eq",
212
+ "arguments": [
213
+ { "type": "responseVariable", "variableRef": "q1...get...q1" },
214
+ { "type": "const", "value": { "type": "reference", "value": "yes" } }
215
+ ]
216
+ }
217
+ }
218
+ ```
219
+
220
+ Component visibility:
221
+ `set-component-display-condition` with `itemId`, `componentId`, and a boolean `expression`.
222
+ For choice options the component id is usually the option id. For form fields or field groups, inspect item details/raw JSON first.
223
+
224
+ Component disabled state:
225
+ `set-component-disabled-condition` with `itemId`, `componentId`, and a boolean `expression`.
226
+
227
+ Validation expressions:
228
+ `set-validation-expression` with `itemId`, `validationKey`, and a boolean `expression`.
229
+ Add a matching validation message translation when respondents need visible feedback.
230
+
231
+ Prefills:
232
+ `set-prefill` with `itemId` and a full prefill object. `when`, when present, must return boolean.
233
+ Prefill source can be `static`, `expression`, `templateValue`, or `previousResponse`.
234
+
235
+ Template values:
236
+ `set-template-value` with `templateKey` and `templateValue`.
237
+ The template expression should match `returnType`; `date2string` also needs `dateFormat`.
238
+
239
+ Clear operations:
240
+ `clear-item-display-condition`, `clear-component-display-condition`,
241
+ `clear-component-disabled-condition`, `clear-validation-expression`,
242
+ `clear-prefill`, and `clear-template-value`.
243
+
244
+ ## Storage Locations
245
+
246
+ Item display condition:
247
+ `/surveyItems/<itemIndex>/displayConditions/root`
248
+
249
+ Component display condition:
250
+ `/surveyItems/<itemIndex>/displayConditions/components/<componentId>`
251
+
252
+ Component disabled condition:
253
+ `/surveyItems/<itemIndex>/disabledConditions/components/<componentId>`
254
+
255
+ Item validation expression:
256
+ `/surveyItems/<itemIndex>/validations/<validationKey>`
257
+
258
+ Prefills:
259
+ `/surveyItems/<itemIndex>/prefills`
260
+
261
+ Template values:
262
+ `/templateValues/<templateValueKey>`
263
+
264
+ ## Prefills
265
+
266
+ Prefill shape:
267
+
268
+ ```json
269
+ {
270
+ "id": "prefill-1",
271
+ "target": { "type": "itemResponse" },
272
+ "when": { "type": "const", "value": { "type": "boolean", "value": true } },
273
+ "apply": "ifEmpty",
274
+ "source": { "type": "static", "value": { "type": "string", "value": "x" } }
275
+ }
276
+ ```
277
+
278
+ Targets:
279
+ - `{ "type": "itemResponse" }`
280
+ - `{ "type": "field", "fieldId": "<fieldId>" }`
281
+ - `{ "type": "embeddedField", "optionId": "<optionId>", "fieldId": "<fieldId>" }`
282
+
283
+ Sources:
284
+ - `static`: direct ResponseValue.
285
+ - `expression`: JsonExpression.
286
+ - `templateValue`: `{ "key": "<templateValueKey>" }`.
287
+ - `previousResponse`: `{ "ref": { "surveyKey"?, "itemId", "target"? }, "ifUnsupported": "skip" }`.
288
+
289
+ ## Template Values
290
+
291
+ Template value shape:
292
+
293
+ ```json
294
+ {
295
+ "type": "default",
296
+ "returnType": "string",
297
+ "expression": { "type": "const", "value": { "type": "string", "value": "x" } }
298
+ }
299
+ ```
300
+
301
+ Date formatting template values use `type: "date2string"`, `returnType: "string"`, and `dateFormat`.
@@ -0,0 +1,364 @@
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": { "items": [], "shuffleItems": false }
13
+ }
14
+ ```
15
+
16
+ Root group additionally has `config.isRoot: true`. Non-root groups should not have `isRoot`.
17
+ Visible group title uses content key `title`.
18
+
19
+ 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.
20
+
21
+ ## Editor Presentation
22
+
23
+ Every item has three distinct naming/presentation surfaces:
24
+ - `key`: stable coding key used in expressions and response paths. Change it with `update-item-key`.
25
+ - `metadata.itemLabel`: internal editor label, not shown to respondents. Change it with `update-item-label`.
26
+ - `metadata.editorItemColor`: editor-only color swatch, not shown to respondents. Change it with `update-item-color`.
27
+
28
+ 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.
29
+
30
+ `update-item-color.color` must be one of the editor palette values: `#404040` (gray), `#b91c1c` (red), `#c2410c` (orange), `#a16207` (amber), `#4d7c0f` (green), `#047857` (teal), `#0369a1` (blue), `#4338ca` (indigo), `#7e22ce` (violet), `#86198f` (magenta), or `#be123c` (rose). Use it only when the user asks for editor organization or color; it must not be used to imply respondent-facing meaning.
31
+
32
+ ## page-break
33
+
34
+ Page-break/structural item. It has no response slots. Visible title uses content key `title`.
35
+
36
+ ## infoItem
37
+
38
+ Non-interactive content item. Config is normally an empty object. Visible body text uses content key `content`.
39
+ For plain text, create or update `content` with typed translation operations.
40
+ For formatted text, create or update `content` with typed translation operations using a `richText` `content` value; see rich-text-content.md.
41
+
42
+ ## choiceItem
43
+
44
+ Interactive choice question:
45
+
46
+ ```json
47
+ {
48
+ "itemType": "choiceItem",
49
+ "config": {
50
+ "id": "<itemId>",
51
+ "maxSelection": 1,
52
+ "shuffleOptions": false,
53
+ "options": [
54
+ { "id": "fuji", "key": "FUJI" }
55
+ ]
56
+ }
57
+ }
58
+ ```
59
+
60
+ Choice conventions:
61
+ - `maxSelection: 1` means single select.
62
+ - `maxSelection: null` means multiple select with no cap.
63
+ - `maxSelection: n` means multiple select capped at n selections.
64
+ - 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.
65
+ - If the user says "choose up to N", "select at most N", or "maximum N", create a `choiceItem` with `maxSelection: N`.
66
+ - If the user says "single choice", "choose one", or options are mutually exclusive by wording, create a `choiceItem` with `maxSelection: 1`.
67
+ - `options[].id` is the stable option id.
68
+ - `options[].key` is optional but should be stable and coding-friendly. When you provide it, use lowercase snake-style keys such as `other` or `opt_1`, matching the editor's choice key normalization.
69
+ - `options[].hasDisplayCondition` is an optional boolean used by the editor to mark options that participate in branching/display logic.
70
+ - Option labels are translations at `options.<optionId>.label`.
71
+ - Do not use provider-specific label metadata such as `labelKey`. CASE resolves every respondent-visible option label from `options.<optionId>.label` translations.
72
+ - 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.
73
+ - 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.
74
+ - The main response slot is `<itemId>...get...<config.id>`; normally `config.id` equals item id.
75
+ - Single select returns `reference`; multiple select returns `reference[]`.
76
+
77
+ Embedded option forms:
78
+ - There is no `allowOther` flag in the current choice item model. Do not use `allowOther` to create free-text "Other" inputs.
79
+ - 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`, `key`, `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.
80
+ - 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.
81
+ - CRITICAL — `embeddedForm` is a FLAT object with exactly two meaningful keys: `id` and `fieldGroups`. Do NOT invent an envelope like `{ "itemType": "formItem", "config": { "fields": [...] } }` — that is not a real shape, it is not schema-validated so it fails silently (no error, no visible field, the option just behaves as if it had no embedded form at all). There is no `itemType`/`config` wrapper anywhere inside `embeddedForm`; `fieldGroups` sits directly on the `embeddedForm` object itself.
82
+ - 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>...`) and response slot (`embedded.<optionId>...`) must match that exact generated id, including case. Setting an explicit lowercase `id` yourself removes this guesswork entirely.
83
+ - To attach input controls to a choice option, put an `embeddedForm` on that option. It uses the same `FormItemConfig` shape as a `formItem` (`id` + `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`).
84
+ - The embedded form renders only while that option is selected. When the option is deselected, existing embedded responses are cleared after user confirmation.
85
+ - 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.
86
+ - Embedded form response slots are `embedded.<optionId>.<fieldKey>`; for example `embedded.other.other_text`.
87
+ - Use `create-item` with embedded form config and matching translations when creating a new choice item. For an existing choice option, first call `survey_inspect` with `action: "raw-json"` and the choice item reference; copy the returned survey-root item path (for example `/surveyItems/<choiceIndex>`) rather than constructing a path from `/config`. Then use one `survey-json-patch` operation with one plural `patches` array for every related configuration and translation update until a dedicated typed helper exists.
88
+ - MANDATORY: every embedded form field needs a matching `embeddedForms.<optionId>.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 embedded form field you defined.
89
+
90
+ Example "Other, please specify" option:
91
+
92
+ ```json
93
+ {
94
+ "id": "other",
95
+ "key": "other",
96
+ "embeddedForm": {
97
+ "id": "other",
98
+ "fieldGroups": [
99
+ {
100
+ "id": "other_details",
101
+ "layout": { "base": 1, "xs": 1, "sm": 1, "md": 1, "xl": 1 },
102
+ "fields": [
103
+ {
104
+ "id": "other_text",
105
+ "key": "other_text",
106
+ "type": "input",
107
+ "required": true,
108
+ "controller": { "type": "input", "inputMode": "text" },
109
+ "validations": []
110
+ }
111
+ ]
112
+ }
113
+ ]
114
+ }
115
+ }
116
+ ```
117
+
118
+ Matching translation keys on the choice item:
119
+ - `options.other.label`: "Other"
120
+ - `embeddedForms.other.fields.other_text.label`: "Please specify"
121
+ - Optional placeholder: `embeddedForms.other.fields.other_text.placeholder`
122
+ - Additional embedded field content keys follow the same form-field families as a top-level form item, for example:
123
+ - `embeddedForms.other.fields.other_text.description`
124
+ - `embeddedForms.other.fields.other_text.error`
125
+ - `embeddedForms.other.fields.other_text.unit`
126
+ - `embeddedForms.other.fields.other_text.checkedLabel`
127
+ - `embeddedForms.other.fields.other_text.uncheckedLabel`
128
+ - `embeddedForms.other.fields.other_text.options.<optionId>.label`
129
+ - `embeddedForms.other.fields.other_text.validations.<validationId>.message`
130
+ - `embeddedForms.other.fieldGroups.<groupId>.title`
131
+ - `embeddedForms.other.fieldGroups.<groupId>.description`
132
+
133
+ ### Full worked example: create a choiceItem with an embedded "Other" text field
134
+
135
+ One `create-item` operation carries the whole item plus every translation it needs — the option labels AND the embedded form field label:
136
+
137
+ ```json
138
+ {
139
+ "kind": "create-item",
140
+ "target": { "parentItemId": "<root-group-id-or-key>", "index": 1 },
141
+ "item": {
142
+ "id": "choice_2",
143
+ "key": "choice_2",
144
+ "itemType": "choiceItem",
145
+ "config": {
146
+ "id": "choice_2",
147
+ "maxSelection": 1,
148
+ "shuffleOptions": false,
149
+ "options": [
150
+ { "id": "strawberry", "key": "strawberry" },
151
+ { "id": "blueberry", "key": "blueberry" },
152
+ {
153
+ "id": "other",
154
+ "key": "other",
155
+ "embeddedForm": {
156
+ "id": "other",
157
+ "fieldGroups": [
158
+ {
159
+ "id": "other_details",
160
+ "layout": { "base": 1, "xs": 1, "sm": 1, "md": 1, "xl": 1 },
161
+ "fields": [
162
+ {
163
+ "id": "other_text",
164
+ "key": "other_text",
165
+ "type": "input",
166
+ "required": true,
167
+ "controller": { "type": "input", "inputMode": "text" },
168
+ "validations": []
169
+ }
170
+ ]
171
+ }
172
+ ]
173
+ }
174
+ }
175
+ ]
176
+ }
177
+ },
178
+ "translations": [
179
+ { "locale": "en", "contentKey": "title", "plainText": "Favorite berry" },
180
+ { "locale": "en", "contentKey": "options.strawberry.label", "plainText": "Strawberry" },
181
+ { "locale": "en", "contentKey": "options.blueberry.label", "plainText": "Blueberry" },
182
+ { "locale": "en", "contentKey": "options.other.label", "plainText": "Other" },
183
+ { "locale": "en", "contentKey": "embeddedForms.other.fields.other_text.label", "plainText": "Please specify" }
184
+ ]
185
+ }
186
+ ```
187
+
188
+ 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.
189
+
190
+ ### Full worked example: create a multiple choice question
191
+
192
+ Use `maxSelection: null` for "select all that apply" / checkbox-style questions:
193
+
194
+ ```json
195
+ {
196
+ "kind": "create-item",
197
+ "target": { "parentItemId": "<root-group-id-or-key>", "index": 1 },
198
+ "item": {
199
+ "id": "industries_worked",
200
+ "key": "industries_worked",
201
+ "itemType": "choiceItem",
202
+ "config": {
203
+ "id": "industries_worked",
204
+ "maxSelection": null,
205
+ "shuffleOptions": false,
206
+ "options": [
207
+ { "id": "healthcare", "key": "HEALTHCARE" },
208
+ { "id": "education", "key": "EDUCATION" },
209
+ { "id": "food_service", "key": "FOOD_SERVICE" }
210
+ ]
211
+ }
212
+ },
213
+ "translations": [
214
+ { "locale": "en", "contentKey": "title", "plainText": "Which industries have you worked in? Select all that apply." },
215
+ { "locale": "en", "contentKey": "options.healthcare.label", "plainText": "Healthcare" },
216
+ { "locale": "en", "contentKey": "options.education.label", "plainText": "Education" },
217
+ { "locale": "en", "contentKey": "options.food_service.label", "plainText": "Food service" }
218
+ ]
219
+ }
220
+ ```
221
+
222
+ ## formItem
223
+
224
+ Interactive form question:
225
+
226
+ ```json
227
+ {
228
+ "itemType": "formItem",
229
+ "config": {
230
+ "id": "<itemId>",
231
+ "fieldGroups": [
232
+ {
233
+ "id": "main",
234
+ "layout": { "base": 1, "xs": 1, "sm": 1, "md": 1, "xl": 1 },
235
+ "fields": []
236
+ }
237
+ ]
238
+ }
239
+ }
240
+ ```
241
+
242
+ CRITICAL — the item's `config` object has exactly two meaningful keys: `id` and `fieldGroups`. `fieldGroups` sits directly on `config` — never nest it under another wrapper, and never put a top-level `fields` array directly on `config` (fields must be inside a `fieldGroups` entry). Fields need at least one non-empty `fieldGroups` entry with an actual `fields` array to be useful; an empty `fieldGroups: [{ "fields": [] }]` is only a starting scaffold, not a finished item.
243
+
244
+ Field group config:
245
+ - `id`: stable group id.
246
+ - `hasDisplayCondition`: optional boolean used by the editor to mark groups that participate in branching/display logic.
247
+ - `layout`: column counts by container breakpoint, using positive integers 1 through 4.
248
+
249
+ Example field group layout:
250
+
251
+ ```json
252
+ {
253
+ "id": "main",
254
+ "hasDisplayCondition": true,
255
+ "layout": { "base": 1, "xs": 1, "sm": 2, "md": 2, "xl": 3 },
256
+ "fields": []
257
+ }
258
+ ```
259
+
260
+ Field group translations:
261
+ - `fieldGroups.<groupId>.title`
262
+ - `fieldGroups.<groupId>.description`
263
+
264
+ Field shape:
265
+
266
+ ```json
267
+ {
268
+ "id": "age",
269
+ "key": "AGE",
270
+ "type": "number",
271
+ "required": true,
272
+ "controller": { "type": "number", "step": 1 },
273
+ "validations": []
274
+ }
275
+ ```
276
+
277
+ Field types and default controllers:
278
+ - `input`: `{ "type": "input", "inputMode": "text" }`
279
+ - `textarea`: `{ "type": "textarea", "rows": 3 }`
280
+ - `number`: `{ "type": "number", "step": 1 }`
281
+ - `datepicker`: `{ "type": "datepicker", "mode": "year-month-day" }`
282
+ - `switch`: `{ "type": "switch", "defaultChecked": false }`
283
+ - `select`: `{ "type": "select", "options": [] }`
284
+ - `slider`: `{ "type": "slider", "min": 0, "max": 100, "step": 1, "defaultValue": 50, "showValue": true }`
285
+
286
+ Supported controller properties by type:
287
+ - `input`: `placeholder`, `inputMode`, `autocomplete`
288
+ - `textarea`: `placeholder`, `rows`
289
+ - `number`: `placeholder`, `step`, `unit`, `defaultValue`
290
+ - `datepicker`: `placeholder`, `mode` where mode is `year`, `year-month`, or `year-month-day`
291
+ - `switch`: `checkedLabel`, `uncheckedLabel`, `defaultChecked`
292
+ - `select`: `placeholder`, `allowClear`, `options`
293
+ - `slider`: `min`, `max`, `step`, `defaultValue`, `showValue`
294
+
295
+ Select controller option shape:
296
+
297
+ ```json
298
+ { "id": "weekly", "value": "weekly" }
299
+ ```
300
+
301
+ Visible select-option labels do NOT live in the controller config. They live in translations at `fields.<fieldId>.options.<optionId>.label`.
302
+ If a select controller option has `id: "minder_7_dagen", value: "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 key/value in the dropdown.
303
+
304
+ Field translation keys:
305
+ - `fields.<fieldId>.label`
306
+ - `fields.<fieldId>.description`
307
+ - `fields.<fieldId>.error`
308
+ - `fields.<fieldId>.placeholder`
309
+ - `fields.<fieldId>.unit`
310
+ - `fields.<fieldId>.checkedLabel`
311
+ - `fields.<fieldId>.uncheckedLabel`
312
+ - `fields.<fieldId>.options.<optionId>.label`
313
+ - `fields.<fieldId>.validations.<validationId>.message`
314
+
315
+ Validation configs use `{ "id", "type", "value"?, "message"? }`.
316
+ Supported validation types: `required`, `minLength`, `maxLength`, `pattern`, `email`, `url`, `integer`, `min`, `max`, `minDate`, `maxDate`, `mustBeTrue`.
317
+
318
+ 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.
319
+ 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`.
320
+
321
+ ### Full worked example: create a formItem with two fields
322
+
323
+ ```json
324
+ {
325
+ "kind": "create-item",
326
+ "target": { "parentItemId": "<root-group-id-or-key>", "index": 1 },
327
+ "item": {
328
+ "id": "form_1",
329
+ "key": "form_1",
330
+ "itemType": "formItem",
331
+ "config": {
332
+ "id": "form_1",
333
+ "fieldGroups": [
334
+ {
335
+ "id": "main",
336
+ "layout": { "base": 1, "xs": 1, "sm": 1, "md": 1, "xl": 1 },
337
+ "fields": [
338
+ {
339
+ "id": "age",
340
+ "key": "AGE",
341
+ "type": "number",
342
+ "required": true,
343
+ "controller": { "type": "number", "step": 1 },
344
+ "validations": []
345
+ },
346
+ {
347
+ "id": "comments",
348
+ "key": "COMMENTS",
349
+ "type": "textarea",
350
+ "controller": { "type": "textarea", "rows": 3 },
351
+ "validations": []
352
+ }
353
+ ]
354
+ }
355
+ ]
356
+ }
357
+ },
358
+ "translations": [
359
+ { "locale": "en", "contentKey": "title", "plainText": "About you" },
360
+ { "locale": "en", "contentKey": "fields.age.label", "plainText": "Your age" },
361
+ { "locale": "en", "contentKey": "fields.comments.label", "plainText": "Any comments?" }
362
+ ]
363
+ }
364
+ ```