@case-framework/survey-assistant 0.6.0 → 0.8.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 (135) hide show
  1. package/README.md +67 -7
  2. package/dist/{attachments-B0hg3vpT.mjs → attachments-BuNni5vB.mjs} +1 -1
  3. package/dist/{attachments-B0hg3vpT.mjs.map → attachments-BuNni5vB.mjs.map} +1 -1
  4. package/dist/{authoring-references-CW38pYzK.mjs → authoring-references-DbuOKerg.mjs} +13 -4
  5. package/dist/authoring-references-DbuOKerg.mjs.map +1 -0
  6. package/dist/{capabilities-DVTfogjv.d.mts → capabilities-CmeeAjEc.d.mts} +3 -4
  7. package/dist/capabilities-CmeeAjEc.d.mts.map +1 -0
  8. package/dist/capabilities-DCMA71PP.mjs +58 -0
  9. package/dist/capabilities-DCMA71PP.mjs.map +1 -0
  10. package/dist/capabilities-default.d.mts +13 -12
  11. package/dist/capabilities-default.d.mts.map +1 -1
  12. package/dist/capabilities-default.mjs +2 -414
  13. package/dist/{constants-BMGTgfjv.d.mts → constants-B6HzpEsx.d.mts} +7 -28
  14. package/dist/constants-B6HzpEsx.d.mts.map +1 -0
  15. package/dist/{constants-0UHGFcRJ.mjs → constants-Ct-vv9cu.mjs} +1 -1
  16. package/dist/{constants-0UHGFcRJ.mjs.map → constants-Ct-vv9cu.mjs.map} +1 -1
  17. package/dist/controller-proxy-D7uQfm5f.d.mts +65 -0
  18. package/dist/controller-proxy-D7uQfm5f.d.mts.map +1 -0
  19. package/dist/{controller-proxy-DMTqer6T.mjs → controller-proxy-DEeFl9IA.mjs} +29 -26
  20. package/dist/controller-proxy-DEeFl9IA.mjs.map +1 -0
  21. package/dist/default-fhSon2RM.mjs +760 -0
  22. package/dist/default-fhSon2RM.mjs.map +1 -0
  23. package/dist/{digest-SJmoYVaK.d.mts → digest-C39WyAWG.d.mts} +2 -3
  24. package/dist/digest-C39WyAWG.d.mts.map +1 -0
  25. package/dist/digest.d.mts +1 -1
  26. package/dist/digest.mjs.map +1 -1
  27. package/dist/{engine-CQwOAcwK.mjs → engine-nXoCWYpQ.mjs} +2821 -2018
  28. package/dist/engine-nXoCWYpQ.mjs.map +1 -0
  29. package/dist/engine.d.mts +3 -3
  30. package/dist/engine.mjs +3 -3
  31. package/dist/{fair-guidance-CRYCRxQm.mjs → fair-guidance-BmO4PswW.mjs} +1 -1
  32. package/dist/{fair-guidance-CRYCRxQm.mjs.map → fair-guidance-BmO4PswW.mjs.map} +1 -1
  33. package/dist/{index-D6nqkz0O.d.mts → index-Ciq7vHIs.d.mts} +121 -197
  34. package/dist/index-Ciq7vHIs.d.mts.map +1 -0
  35. package/dist/{index-DvTqML-0.d.mts → index-D7wT6P3t.d.mts} +294 -63
  36. package/dist/index-D7wT6P3t.d.mts.map +1 -0
  37. package/dist/index-WB5kfnz0.d.mts +348 -0
  38. package/dist/index-WB5kfnz0.d.mts.map +1 -0
  39. package/dist/{index-B0lxsAzx.d.mts → index-r5riv4md.d.mts} +50 -29
  40. package/dist/index-r5riv4md.d.mts.map +1 -0
  41. package/dist/{lifecycle-BmGAIUlv.d.mts → lifecycle-Dm8u7QFh.d.mts} +40 -18
  42. package/dist/lifecycle-Dm8u7QFh.d.mts.map +1 -0
  43. package/dist/{memory-thread-repository-CjFGVNZh.mjs → memory-thread-repository-DAnOGIB3.mjs} +89 -34
  44. package/dist/memory-thread-repository-DAnOGIB3.mjs.map +1 -0
  45. package/dist/{protocol-DJUP4gc0.mjs → protocol-CF4Bh_Ch.mjs} +182 -50
  46. package/dist/protocol-CF4Bh_Ch.mjs.map +1 -0
  47. package/dist/protocol.d.mts +4 -4
  48. package/dist/protocol.mjs +3 -3
  49. package/dist/react-C_GY-Fsc.mjs +1727 -0
  50. package/dist/react-C_GY-Fsc.mjs.map +1 -0
  51. package/dist/react-integration.d.mts +2 -2
  52. package/dist/react-integration.mjs +2 -2
  53. package/dist/react.d.mts +3 -3
  54. package/dist/react.mjs +3 -3
  55. package/dist/references/assistant-operations.md +143 -68
  56. package/dist/references/core-rules.md +6 -3
  57. package/dist/references/embedded-forms.md +34 -14
  58. package/dist/references/expressions.md +238 -48
  59. package/dist/references/fair-by-design.md +9 -9
  60. package/dist/references/item-types.md +306 -72
  61. package/dist/references/localization.md +5 -4
  62. package/dist/references/response-variables.md +38 -0
  63. package/dist/references/rich-text-content.md +60 -27
  64. package/dist/references/source-material-surveys.md +13 -5
  65. package/dist/references/survey-data-model.md +32 -9
  66. package/dist/{request-context-BjTHNcDd.mjs → request-context-Dg4jSwN1.mjs} +4 -10
  67. package/dist/request-context-Dg4jSwN1.mjs.map +1 -0
  68. package/dist/{request-guardrails-TmgQtqp4.mjs → request-guardrails-C0ugBOp0.mjs} +6 -5
  69. package/dist/request-guardrails-C0ugBOp0.mjs.map +1 -0
  70. package/dist/server-agent.d.mts +53 -44
  71. package/dist/server-agent.d.mts.map +1 -1
  72. package/dist/server-agent.mjs +48 -27
  73. package/dist/server-agent.mjs.map +1 -1
  74. package/dist/server-runtime.d.mts +36 -49
  75. package/dist/server-runtime.d.mts.map +1 -1
  76. package/dist/server-runtime.mjs +17 -12
  77. package/dist/server-runtime.mjs.map +1 -1
  78. package/dist/server-tasks.d.mts +2 -2
  79. package/dist/server-tasks.mjs +2 -313
  80. package/dist/server-tools.d.mts +1 -1
  81. package/dist/server-tools.mjs +1 -1
  82. package/dist/server.d.mts +13 -36
  83. package/dist/server.d.mts.map +1 -1
  84. package/dist/server.mjs +5 -5
  85. package/dist/server.mjs.map +1 -1
  86. package/dist/storage-postgres.d.mts +9 -25
  87. package/dist/storage-postgres.d.mts.map +1 -1
  88. package/dist/storage-postgres.mjs +5 -4
  89. package/dist/storage-postgres.mjs.map +1 -1
  90. package/dist/{tasks-DxzD25Zc.d.mts → tasks-B9h3LPam.d.mts} +19 -35
  91. package/dist/tasks-B9h3LPam.d.mts.map +1 -0
  92. package/dist/tasks-BZEiwgxA.mjs +396 -0
  93. package/dist/tasks-BZEiwgxA.mjs.map +1 -0
  94. package/dist/thread-documents-DrHbaXqP.d.mts +119 -0
  95. package/dist/thread-documents-DrHbaXqP.d.mts.map +1 -0
  96. package/dist/{thread-handlers-AbQbt4sX.d.mts → thread-handlers-Bef0td9N.d.mts} +10 -40
  97. package/dist/thread-handlers-Bef0td9N.d.mts.map +1 -0
  98. package/dist/{tools-CFh35R4d.mjs → tools-CEsv-WoO.mjs} +705 -578
  99. package/dist/tools-CEsv-WoO.mjs.map +1 -0
  100. package/dist/ui.css +1 -1
  101. package/dist/ui.d.mts +17 -55
  102. package/dist/ui.d.mts.map +1 -1
  103. package/dist/ui.mjs +759 -171
  104. package/dist/ui.mjs.map +1 -1
  105. package/docs/integration.md +51 -1
  106. package/package.json +49 -42
  107. package/dist/authoring-references-CW38pYzK.mjs.map +0 -1
  108. package/dist/capabilities-DVTfogjv.d.mts.map +0 -1
  109. package/dist/capabilities-default.mjs.map +0 -1
  110. package/dist/constants-BMGTgfjv.d.mts.map +0 -1
  111. package/dist/controller-proxy-DMTqer6T.mjs.map +0 -1
  112. package/dist/controller-proxy-mRvez2qw.d.mts +0 -85
  113. package/dist/controller-proxy-mRvez2qw.d.mts.map +0 -1
  114. package/dist/digest-SJmoYVaK.d.mts.map +0 -1
  115. package/dist/engine-CQwOAcwK.mjs.map +0 -1
  116. package/dist/form-authoring-kQDgC5oz.mjs +0 -155
  117. package/dist/form-authoring-kQDgC5oz.mjs.map +0 -1
  118. package/dist/index-B0lxsAzx.d.mts.map +0 -1
  119. package/dist/index-BS-Xjk1h.d.mts +0 -224
  120. package/dist/index-BS-Xjk1h.d.mts.map +0 -1
  121. package/dist/index-D6nqkz0O.d.mts.map +0 -1
  122. package/dist/index-DvTqML-0.d.mts.map +0 -1
  123. package/dist/lifecycle-BmGAIUlv.d.mts.map +0 -1
  124. package/dist/memory-thread-repository-CjFGVNZh.mjs.map +0 -1
  125. package/dist/protocol-DJUP4gc0.mjs.map +0 -1
  126. package/dist/react-Dqlmasrs.mjs +0 -768
  127. package/dist/react-Dqlmasrs.mjs.map +0 -1
  128. package/dist/request-context-BjTHNcDd.mjs.map +0 -1
  129. package/dist/request-guardrails-TmgQtqp4.mjs.map +0 -1
  130. package/dist/server-tasks.mjs.map +0 -1
  131. package/dist/tasks-DxzD25Zc.d.mts.map +0 -1
  132. package/dist/thread-documents-rmd6nyX9.d.mts +0 -107
  133. package/dist/thread-documents-rmd6nyX9.d.mts.map +0 -1
  134. package/dist/thread-handlers-AbQbt4sX.d.mts.map +0 -1
  135. package/dist/tools-CFh35R4d.mjs.map +0 -1
@@ -9,7 +9,10 @@ Structure/container item:
9
9
  ```json
10
10
  {
11
11
  "itemType": "group",
12
- "config": { "items": [], "shuffleItems": false }
12
+ "config": {
13
+ "items": [],
14
+ "shuffleItems": false
15
+ }
13
16
  }
14
17
  ```
15
18
 
@@ -22,7 +25,7 @@ Use a group when its children form a meaningful, independently managed section o
22
25
 
23
26
  Every item has three distinct naming/presentation surfaces:
24
27
 
25
- - `key`: stable coding key used in expressions and response paths. Change it with `update-item-key`.
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`.
26
29
  - `metadata.itemLabel`: internal editor label, not shown to respondents. Change it with `update-item-label`.
27
30
  - `metadata.editorItemColor`: editor-only color swatch, not shown to respondents. Change it with `update-item-color`.
28
31
 
@@ -38,7 +41,29 @@ Page-break/structural item. It has no response slots. Visible title uses content
38
41
 
39
42
  Non-interactive content item. Config is normally an empty object. Visible body text uses content key `content`.
40
43
  For plain text, create or update `content` with typed translation operations.
41
- For formatted text, create or update `content` with typed translation operations using a `richText` `content` value; see rich-text-content.md.
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.
42
67
 
43
68
  ## choiceItem
44
69
 
@@ -51,7 +76,12 @@ Interactive choice question:
51
76
  "id": "<itemId>",
52
77
  "maxSelection": 1,
53
78
  "shuffleOptions": false,
54
- "options": [{ "id": "fuji", "key": "FUJI" }]
79
+ "options": [
80
+ {
81
+ "id": "fuji",
82
+ "code": "FUJI"
83
+ }
84
+ ]
55
85
  }
56
86
  }
57
87
  ```
@@ -65,31 +95,27 @@ Choice conventions:
65
95
  - If the user says "choose up to N", "select at most N", or "maximum N", create a `choiceItem` with `maxSelection: N`.
66
96
  - If the user says "single choice", "choose one", or options are mutually exclusive by wording, create a `choiceItem` with `maxSelection: 1`.
67
97
  - `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.
98
+ - `options[].code` is an export code. Responses and expressions continue to use the exact option ID; codes, labels and IDs are independent.
69
99
  - `options[].hasDisplayCondition` is an optional boolean used by the editor to mark options that participate in branching/display logic.
70
100
  - Option labels are translations at `options.<optionId>.label`.
71
101
  - Do not use provider-specific label metadata such as `labelKey`. CASE resolves every respondent-visible option label from `options.<optionId>.label` translations.
72
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.
73
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.
74
- - The main response slot is `<itemId>...get...<config.id>`. For a new choice item, always set
75
- `config.id` to the exact same value as `item.id`; do not invent a second semantic name from the
76
- question wording and do not add ordering suffixes such as `q1`. The choice response-slot ID has no
77
- direct editor control and expressions refer to it exactly, so changing it later requires updating
78
- every reference in the same change.
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.
79
105
  - Single select returns `reference`; multiple select returns `reference[]`.
80
106
 
81
107
  Embedded option forms:
82
108
 
83
109
  - There is no `allowOther` flag in the current choice item model. Do not use `allowOther` to create free-text "Other" inputs.
84
- - 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.
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.
85
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.
86
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.
87
- - 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.
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.
88
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`).
89
- - 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 rekeying fields, or changing groups/layouts, is a one-way transition to `custom`; preserve the existing field and translations during that transition.
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.
90
116
  - The embedded form renders only while that option is selected. When the option is deselected, existing embedded responses are cleared after user confirmation.
91
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.
92
- - Embedded form response slots are `embedded.<optionId>.<fieldKey>`; for example `embedded.other.other_text`.
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.
93
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.
94
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.
95
121
 
@@ -98,27 +124,36 @@ Example "Other, please specify" option:
98
124
  ```json
99
125
  {
100
126
  "id": "other",
101
- "key": "other",
102
127
  "embeddedForm": {
103
128
  "id": "other",
104
129
  "authoringMode": "simple",
105
130
  "fieldGroups": [
106
131
  {
107
132
  "id": "other_details",
108
- "layout": { "base": 1, "xs": 1, "sm": 1, "md": 1, "xl": 1 },
133
+ "layout": {
134
+ "base": 1,
135
+ "xs": 1,
136
+ "sm": 1,
137
+ "md": 1,
138
+ "xl": 1
139
+ },
109
140
  "fields": [
110
141
  {
111
142
  "id": "other_text",
112
- "key": "other_text",
113
143
  "type": "input",
114
144
  "required": true,
115
- "controller": { "type": "input", "inputMode": "text" },
116
- "validations": []
145
+ "controller": {
146
+ "type": "input",
147
+ "inputMode": "text"
148
+ },
149
+ "validations": [],
150
+ "variableName": "other_text"
117
151
  }
118
152
  ]
119
153
  }
120
154
  ]
121
- }
155
+ },
156
+ "code": "other"
122
157
  }
123
158
  ```
124
159
 
@@ -145,50 +180,84 @@ One `create-item` operation carries the whole item plus every translation it nee
145
180
  ```json
146
181
  {
147
182
  "kind": "create-item",
148
- "target": { "parentItemId": "<root-group-id-or-key>", "index": 1 },
183
+ "target": {
184
+ "parentItemId": "<root-group-id>",
185
+ "index": 1
186
+ },
149
187
  "item": {
150
188
  "id": "choice_2",
151
- "key": "choice_2",
152
189
  "itemType": "choiceItem",
153
190
  "config": {
154
191
  "id": "choice_2",
155
192
  "maxSelection": 1,
156
193
  "shuffleOptions": false,
157
194
  "options": [
158
- { "id": "strawberry", "key": "strawberry" },
159
- { "id": "blueberry", "key": "blueberry" },
195
+ {
196
+ "id": "strawberry",
197
+ "code": "strawberry"
198
+ },
199
+ {
200
+ "id": "blueberry",
201
+ "code": "blueberry"
202
+ },
160
203
  {
161
204
  "id": "other",
162
- "key": "other",
163
205
  "embeddedForm": {
164
206
  "id": "other",
165
207
  "authoringMode": "simple",
166
208
  "fieldGroups": [
167
209
  {
168
210
  "id": "other_details",
169
- "layout": { "base": 1, "xs": 1, "sm": 1, "md": 1, "xl": 1 },
211
+ "layout": {
212
+ "base": 1,
213
+ "xs": 1,
214
+ "sm": 1,
215
+ "md": 1,
216
+ "xl": 1
217
+ },
170
218
  "fields": [
171
219
  {
172
220
  "id": "other_text",
173
- "key": "other_text",
174
221
  "type": "input",
175
222
  "required": true,
176
- "controller": { "type": "input", "inputMode": "text" },
177
- "validations": []
223
+ "controller": {
224
+ "type": "input",
225
+ "inputMode": "text"
226
+ },
227
+ "validations": [],
228
+ "variableName": "other_text"
178
229
  }
179
230
  ]
180
231
  }
181
232
  ]
182
- }
233
+ },
234
+ "code": "other"
183
235
  }
184
- ]
236
+ ],
237
+ "variableName": "choice_2"
185
238
  }
186
239
  },
187
240
  "translations": [
188
- { "locale": "en", "contentKey": "title", "plainText": "Favorite berry" },
189
- { "locale": "en", "contentKey": "options.strawberry.label", "plainText": "Strawberry" },
190
- { "locale": "en", "contentKey": "options.blueberry.label", "plainText": "Blueberry" },
191
- { "locale": "en", "contentKey": "options.other.label", "plainText": "Other" },
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
+ },
192
261
  {
193
262
  "locale": "en",
194
263
  "contentKey": "embeddedForms.other.fields.other_text.label",
@@ -207,20 +276,32 @@ Use `maxSelection: null` for "select all that apply" / checkbox-style questions:
207
276
  ```json
208
277
  {
209
278
  "kind": "create-item",
210
- "target": { "parentItemId": "<root-group-id-or-key>", "index": 1 },
279
+ "target": {
280
+ "parentItemId": "<root-group-id>",
281
+ "index": 1
282
+ },
211
283
  "item": {
212
284
  "id": "industries_worked",
213
- "key": "industries_worked",
214
285
  "itemType": "choiceItem",
215
286
  "config": {
216
287
  "id": "industries_worked",
217
288
  "maxSelection": null,
218
289
  "shuffleOptions": false,
219
290
  "options": [
220
- { "id": "healthcare", "key": "HEALTHCARE" },
221
- { "id": "education", "key": "EDUCATION" },
222
- { "id": "food_service", "key": "FOOD_SERVICE" }
223
- ]
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"
224
305
  }
225
306
  },
226
307
  "translations": [
@@ -229,9 +310,21 @@ Use `maxSelection: null` for "select all that apply" / checkbox-style questions:
229
310
  "contentKey": "title",
230
311
  "plainText": "Which industries have you worked in? Select all that apply."
231
312
  },
232
- { "locale": "en", "contentKey": "options.healthcare.label", "plainText": "Healthcare" },
233
- { "locale": "en", "contentKey": "options.education.label", "plainText": "Education" },
234
- { "locale": "en", "contentKey": "options.food_service.label", "plainText": "Food service" }
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
+ }
235
328
  ]
236
329
  }
237
330
  ```
@@ -249,7 +342,13 @@ Interactive form question:
249
342
  "fieldGroups": [
250
343
  {
251
344
  "id": "main",
252
- "layout": { "base": 1, "xs": 1, "sm": 1, "md": 1, "xl": 1 },
345
+ "layout": {
346
+ "base": 1,
347
+ "xs": 1,
348
+ "sm": 1,
349
+ "md": 1,
350
+ "xl": 1
351
+ },
253
352
  "fields": []
254
353
  }
255
354
  ]
@@ -264,8 +363,8 @@ Authoring modes and Add item presets:
264
363
  - `Text` → `input`, `Long text` → `textarea`, `Number` → `number`, `Slider` → `slider`, `Dropdown` → `select`, `Date` → `datepicker`, and `Switch` → `switch`.
265
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.
266
365
  - `Blank form` uses `authoringMode: "custom"`. Any finished multi-field/group form also uses custom mode.
267
- - Simple mode exposes the existing field's label, description, required state, and compatible controller settings. It does not expose groups, layouts, field reordering, field keys, or field-type switching.
268
- - Adding/removing/moving fields or groups, changing layout, or changing the sole field's key/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.
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.
269
368
  - If a stored simple form lacks its sole first field, treat it as custom without deleting any remaining data.
270
369
 
271
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.
@@ -282,7 +381,13 @@ Example field group layout:
282
381
  {
283
382
  "id": "main",
284
383
  "hasDisplayCondition": true,
285
- "layout": { "base": 1, "xs": 1, "sm": 2, "md": 2, "xl": 3 },
384
+ "layout": {
385
+ "base": 1,
386
+ "xs": 1,
387
+ "sm": 2,
388
+ "md": 2,
389
+ "xl": 3
390
+ },
286
391
  "fields": []
287
392
  }
288
393
  ```
@@ -297,21 +402,18 @@ Field shape:
297
402
  ```json
298
403
  {
299
404
  "id": "age",
300
- "key": "AGE",
301
405
  "type": "number",
302
406
  "required": true,
303
- "controller": { "type": "number", "step": 1 },
304
- "validations": []
407
+ "controller": {
408
+ "type": "number",
409
+ "step": 1
410
+ },
411
+ "validations": [],
412
+ "variableName": "age"
305
413
  }
306
414
  ```
307
415
 
308
- `key` is the field's exact response-slot identifier. Choose a stable coding key for the field's
309
- role within this form item. Do not copy the question wording, duplicate context already present in
310
- the item key, or add ordering suffixes such as `q1`. For a single-field form, concise keys such as
311
- `response`, `selection`, `value`, or `other_text` are often appropriate. Keys only need to be unique
312
- within the form item, so the same general key can be reused in another item. A user can edit the
313
- field key in the form editor, but changing it also changes the response-slot identifier; any
314
- expressions that use the old slot must be updated in the same change.
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.
315
417
 
316
418
  Field types and default controllers:
317
419
 
@@ -336,11 +438,14 @@ Supported controller properties by type:
336
438
  Select controller option shape:
337
439
 
338
440
  ```json
339
- { "id": "weekly", "value": "weekly" }
441
+ {
442
+ "id": "weekly",
443
+ "code": "weekly"
444
+ }
340
445
  ```
341
446
 
342
447
  Visible select-option labels do NOT live in the controller config. They live in translations at `fields.<fieldId>.options.<optionId>.label`.
343
- 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.
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.
344
449
 
345
450
  Field translation keys:
346
451
 
@@ -365,10 +470,12 @@ MANDATORY: every `select` option needs a matching `fields.<fieldId>.options.<opt
365
470
  ```json
366
471
  {
367
472
  "kind": "create-item",
368
- "target": { "parentItemId": "<root-group-id-or-key>", "index": 1 },
473
+ "target": {
474
+ "parentItemId": "<root-group-id>",
475
+ "index": 1
476
+ },
369
477
  "item": {
370
478
  "id": "form_1",
371
- "key": "form_1",
372
479
  "itemType": "formItem",
373
480
  "config": {
374
481
  "id": "form_1",
@@ -376,22 +483,34 @@ MANDATORY: every `select` option needs a matching `fields.<fieldId>.options.<opt
376
483
  "fieldGroups": [
377
484
  {
378
485
  "id": "main",
379
- "layout": { "base": 1, "xs": 1, "sm": 1, "md": 1, "xl": 1 },
486
+ "layout": {
487
+ "base": 1,
488
+ "xs": 1,
489
+ "sm": 1,
490
+ "md": 1,
491
+ "xl": 1
492
+ },
380
493
  "fields": [
381
494
  {
382
495
  "id": "age",
383
- "key": "AGE",
384
496
  "type": "number",
385
497
  "required": true,
386
- "controller": { "type": "number", "step": 1 },
387
- "validations": []
498
+ "controller": {
499
+ "type": "number",
500
+ "step": 1
501
+ },
502
+ "validations": [],
503
+ "variableName": "age"
388
504
  },
389
505
  {
390
506
  "id": "comments",
391
- "key": "COMMENTS",
392
507
  "type": "textarea",
393
- "controller": { "type": "textarea", "rows": 3 },
394
- "validations": []
508
+ "controller": {
509
+ "type": "textarea",
510
+ "rows": 3
511
+ },
512
+ "validations": [],
513
+ "variableName": "comments"
395
514
  }
396
515
  ]
397
516
  }
@@ -399,9 +518,124 @@ MANDATORY: every `select` option needs a matching `fields.<fieldId>.options.<opt
399
518
  }
400
519
  },
401
520
  "translations": [
402
- { "locale": "en", "contentKey": "title", "plainText": "About you" },
403
- { "locale": "en", "contentKey": "fields.age.label", "plainText": "Your age" },
404
- { "locale": "en", "contentKey": "fields.comments.label", "plainText": "Any comments?" }
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
+ }
405
536
  ]
406
537
  }
407
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.
@@ -17,10 +17,11 @@ Use this reference when the user asks to translate, localize, add a language, or
17
17
  ## Scope A Complete Localization Correctly
18
18
 
19
19
  1. Use `survey_inspect` with `action: "localization"`, the source locale, and the target locale. Page until `nextCursor` is absent; this covers existing respondent-visible item and survey translation surfaces without one item-details call per item.
20
- 2. Inspect item configuration separately only when the localization surface result or an audit indicates that the source survey itself is missing a visible content key. This includes choice-option labels, form-field labels, select-option labels, embedded-form labels, and validation copy.
21
- 3. Preserve the source language while writing each target-language value under the target locale. Do not replace source content or translation containers.
22
- 4. Use `update-item-translation` for ordinary item content. Use typed choice operations only when also changing the choice structure. Use a focused raw patch only for a supported survey-level translation surface that has no typed operation.
23
- 5. Before proposing, check that every visible label implied by the inspected item configuration has a target-locale translation. A title alone is not a complete localization of a choice or form item.
20
+ 2. Treat `sourceText` and `targetText` as previews. When `sourceTruncated` is true for plain text, retrieve `sourceTextPath` through `survey_inspect` action `raw-json`, then follow `nextTextOffset` using `textOffset` and that same path until all segments are read. Translate the complete source, never only the preview. Rich-text `sourceContent` already contains the full canonical value. `targetPresent` proves presence, not translation fidelity or completeness.
21
+ 3. Inspect item configuration separately only when the localization surface result or an audit indicates that the source survey itself is missing a visible content key. This includes choice-option labels, form-field labels, select-option labels, embedded-form labels, and validation copy.
22
+ 4. Preserve the source language while writing each target-language value under the target locale. Do not replace source content or translation containers.
23
+ 5. Use `update-item-translation` for ordinary item content. Use typed choice operations only when also changing the choice structure. Use a focused raw patch only for a supported survey-level translation surface that has no typed operation.
24
+ 6. Before proposing, check that every visible label implied by the inspected item configuration has a target-locale translation. A title alone is not a complete localization of a choice or form item.
24
25
 
25
26
  ## Bounded Execution
26
27