@choiceapps/communityservices-sdk 0.7.0 → 0.8.1

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 (43) hide show
  1. package/API.md +245 -0
  2. package/CHANGELOG.md +15 -0
  3. package/browser/enrollment-workflow.d.mts +4 -2
  4. package/browser/enrollment-workflow.mjs +28 -14
  5. package/browser/entry-correction.mjs +7 -7
  6. package/browser/index.d.mts +4 -1
  7. package/browser/index.mjs +1 -1
  8. package/browser/playwright-session.mjs +20 -5
  9. package/browser/workflow-error-codes.mjs +3 -0
  10. package/dist/cjs/picklist-labels.d.ts +22 -0
  11. package/dist/cjs/picklist-labels.d.ts.map +1 -0
  12. package/dist/cjs/picklist-labels.js +35 -0
  13. package/dist/cjs/picklist-labels.js.map +1 -0
  14. package/dist/cjs/v2.d.ts +2 -0
  15. package/dist/cjs/v2.d.ts.map +1 -1
  16. package/dist/cjs/v2.js +3 -0
  17. package/dist/cjs/v2.js.map +1 -1
  18. package/dist/cjs/v3.d.ts +2 -0
  19. package/dist/cjs/v3.d.ts.map +1 -1
  20. package/dist/cjs/v3.js +3 -0
  21. package/dist/cjs/v3.js.map +1 -1
  22. package/dist/esm/picklist-labels.d.ts +22 -0
  23. package/dist/esm/picklist-labels.d.ts.map +1 -0
  24. package/dist/esm/picklist-labels.js +32 -0
  25. package/dist/esm/picklist-labels.js.map +1 -0
  26. package/dist/esm/v2.d.ts +2 -0
  27. package/dist/esm/v2.d.ts.map +1 -1
  28. package/dist/esm/v2.js +1 -0
  29. package/dist/esm/v2.js.map +1 -1
  30. package/dist/esm/v3.d.ts +2 -0
  31. package/dist/esm/v3.d.ts.map +1 -1
  32. package/dist/esm/v3.js +1 -0
  33. package/dist/esm/v3.js.map +1 -1
  34. package/docs/assessments.md +23 -3
  35. package/docs/capabilities.md +8 -2
  36. package/docs/enrollments.md +4 -2
  37. package/docs/reference.md +2 -2
  38. package/docs/troubleshooting.md +1 -1
  39. package/docs/vendor-issues/lookup-write-labels.md +13 -0
  40. package/docs/vendor-issues/yes-no-answers.md +11 -3
  41. package/examples/README.md +1 -0
  42. package/examples/catalog-write-labels.mts +15 -0
  43. package/package.json +1 -1
package/API.md CHANGED
@@ -84,3 +84,248 @@ Both versions have the methods below. Import from `@choiceapps/communityservices
84
84
  | `tenants.update` | PUT | `/tenants/UpdateTenantRecord` | |
85
85
  | `tenants.updateByCode` | PUT | `/tenants/UpdateTenantRecord/{tenant_code}` | |
86
86
  | `tenants.delete` | DELETE | `/tenants/DeleteTenantRecord` | |
87
+
88
+ ## Enrollment workflow errors
89
+
90
+ This SDK-owned code inventory is generated from the workflow guards and stable reason map. These codes are separate from the REST Swagger operations above. Classify errors by `code`, never by message text. Existing codes remain stable when wording changes; later releases may add codes. Application-supplied constructor codes are outside this inventory.
91
+
92
+ `EnrollmentWorkflowError.operation` identifies the SDK operation. `committed: false` means that operation has not attempted a WellSky record write; it does not promise that no local journal exists or that another operation wrote nothing. `committed: "unknown"` preserves an attempted or unreadable prior outcome. Never choose a new key solely from an HTTP status or unchanged read.
93
+
94
+ A preflight refusal after enrollment creation can identify `step: "scalars"` while the enrollment is already saved. Keep the same key and reconcile the existing journal. A failed `resolveEntryCorrection` can have `committed: false` for resolution itself and `originalCommitted: "unknown"` for the original correction. See [recovery guidance](docs/troubleshooting.md#workflow-error-codes).
95
+
96
+ | Code | Reason or meaning |
97
+ | --- | --- |
98
+ | `ACTIVE_CLIENT_REQUIRED` | active client required |
99
+ | `ACTIVE_HOUSEHOLD_REQUIRED` | active household required |
100
+ | `ACTIVE_HOUSEHOLD_REQUIRED_FOR_ADOPTION` | active household required for adoption |
101
+ | `ACTIVE_OPERATIONAL_PROGRAM_REQUIRED` | active operational program required |
102
+ | `ADDED_ENROLLMENT_IDENTITY_CHANGED` | added enrollment identity changed |
103
+ | `ADDITION_REQUIRES_NEW_HOUSEHOLD_MEMBERS` | addition requires new household members |
104
+ | `ADDITIONAL_MEMBER_ALREADY_HAS_AN_INDEPENDENT_ENROLLMENT` | additional member already has an independent enrollment |
105
+ | `ADDITIONAL_MEMBER_ASSESSMENT_CONFIGURATION_CHANGED` | additional member assessment configuration changed |
106
+ | `ADDITIONAL_MEMBER_ENTRY_MUST_FOLLOW_ORIGINAL_ENTRY` | additional member entry must follow original entry |
107
+ | `ADOPTED_ENROLLMENT_IDENTITY_OR_CONFIGURATION_CHANGED` | adopted enrollment identity or configuration changed |
108
+ | `ADOPTION_COMPLETE_COHORT_REQUIRED` | adoption requires the complete linked enrollment cohort |
109
+ | `ADOPTION_ENROLLMENT_CONTEXT_MISMATCH` | adoption enrollment context mismatch |
110
+ | `ADOPTION_EXIT_PRECEDES_ENTRY` | adoption exit precedes entry |
111
+ | `ADOPTION_HOUSEHOLD_HEAD_MISMATCH` | adoption household head mismatch |
112
+ | `ADOPTION_HOUSEHOLD_MEMBERSHIP_MISMATCH` | adoption household membership mismatch |
113
+ | `ADOPTION_HOUSEHOLD_REQUIRED` | linked household enrollment requires adoptHouseholdEnrollment |
114
+ | `ADOPTION_KEY_CONFLICTS_WITH_PREVIOUS_INTENT` | adoption key conflicts with previous intent |
115
+ | `ADOPTION_REQUIRES_THE_ORIGINAL_ENROLLMENT_HEAD_RELATIONSHIP` | adoption requires the original enrollment head relationship |
116
+ | `AMBIGUOUS_ASSESSMENT_READ` | ambiguous assessment read |
117
+ | `AMBIGUOUS_CLOSURE_SNAPSHOT` | ambiguous closure snapshot |
118
+ | `AMBIGUOUS_CORRECTION_ASSESSMENT_READ` | ambiguous correction assessment read |
119
+ | `AMBIGUOUS_CORRECTION_ROW` | ambiguous correction row |
120
+ | `AMBIGUOUS_CORRECTION_ROW_CELL` | ambiguous correction row cell |
121
+ | `AMBIGUOUS_HOUSEHOLD_MEMBERSHIP` | ambiguous household membership |
122
+ | `ANOTHER_UNFINISHED_HOUSEHOLD_WORKFLOW_MUST_BE_RECONCILED_FIRST` | another unfinished household workflow must be reconciled first |
123
+ | `ANOTHER_UNFINISHED_WORKFLOW_MUST_BE_RECONCILED_FIRST` | another unfinished workflow must be reconciled first |
124
+ | `ANSWER_ENCODING_UNAVAILABLE` | answer encoding unavailable |
125
+ | `ASSESSMENT_FIELD_UNCOVERED` | assessment field uncovered |
126
+ | `ASSESSMENT_LOCATIONS_MUST_BE_A_MAP_BY_ENTRY_EXIT_TYPE` | assessment locations must be a map by Entry/Exit type |
127
+ | `ASSESSMENT_LOCATIONS_REQUIRED` | assessment locations required |
128
+ | `ASSESSMENT_METADATA_INCOMPLETE` | assessment metadata incomplete |
129
+ | `ASSESSMENT_PLAN_REQUIRED` | assessment plan required |
130
+ | `ATTEMPTED_CORRECTION_REQUIRED_FOR_RESOLUTION` | attempted correction required for resolution |
131
+ | `AUTHORITATIVE_ADOPTION_EXIT_DATE_REQUIRED` | authoritative adoption exit date required |
132
+ | `AUTHORITATIVE_ENROLLMENT_SESSION_REQUIRED_FOR_ADOPTION` | authoritative enrollment session required for adoption |
133
+ | `AUTHORITATIVE_EXIT_DETAILS_READER_REQUIRED` | authoritative exit details reader required |
134
+ | `AUTHORITATIVE_EXIT_READ_REQUIRED` | authoritative exit read required |
135
+ | `AUTHORITATIVE_HEAD_OF_HOUSEHOLD_EXIT_DATES_REQUIRED` | authoritative head of household exit dates required |
136
+ | `AUTHORITATIVE_INDIVIDUAL_EXIT_READ_REQUIRED` | authoritative individual exit read required |
137
+ | `CARRY_FORWARD_BIRTH_DATE_CHANGED_SINCE_ENROLLMENT` | carry-forward birth date changed since enrollment |
138
+ | `CARRY_FORWARD_BIRTH_DATE_IS_EMPTY_OR_FOLLOWS_ASSESSMENT_DATE` | carry-forward birth date is empty or follows assessment date |
139
+ | `CARRY_FORWARD_FIELD_IS_EMPTY` | carry-forward field is empty |
140
+ | `CLIENT_ID_REQUIRED` | client ID required |
141
+ | `CLIENT_PROVIDER_AND_SUPPORTED_ENTRY_EXIT_TYPE_REQUIRED` | client, provider and supported Entry/Exit type required |
142
+ | `CONFLICTING_FIELD_COVERAGE` | conflicting field coverage |
143
+ | `CORRECTION_ANSWER_COULD_NOT_BE_ENCODED` | correction answer could not be encoded |
144
+ | `CORRECTION_KEY_CONFLICTS_WITH_PREVIOUS_INTENT` | correction key conflicts with previous intent |
145
+ | `CORRECTION_LOOKUP_COULD_NOT_BE_RESOLVED` | correction lookup could not be resolved |
146
+ | `CORRECTION_REQUIRES_AN_EXISTING_ENTRY_ROW_KEY_AND_MATCHING_PARENT` | correction requires an existing entry row key and matching parent |
147
+ | `CORRECTION_ROW_METADATA_INCOMPLETE` | correction row metadata incomplete |
148
+ | `CORRECTION_SCALAR_READ_MISSING` | correction scalar read missing |
149
+ | `CORRECTION_STATE_CHANGED_DURING_RESOLUTION` | correction state changed during resolution |
150
+ | `CORRECTION_STATE_DIFFERS_FROM_ITS_PRE_WRITE_SNAPSHOT_NON_APPLICATION_CANNOT_BE_RESOLVED` | correction state differs from its pre-write snapshot; non-application cannot be resolved |
151
+ | `CORRECTION_SUBASSESSMENT_READ_MISSING` | correction subassessment read missing |
152
+ | `CORRECTION_WAS_RESOLVED_AS_NOT_APPLIED_USE_A_NEW_KEY_FOR_A_CORRECTED_INTENT` | correction was resolved as not applied; use a new key for a corrected intent |
153
+ | `DESTINATION_AND_REASON_FOR_LEAVING_REQUIRED` | destination and reason for leaving required |
154
+ | `DOB_AND_HOUSEHOLD_HEAD_CORRECTIONS_REQUIRE_THEIR_DEDICATED_WORKFLOWS` | DOB and household head corrections require their dedicated workflows |
155
+ | `DOB_HELPER_REQUIRED_FOR_BIRTH_DATE_ANSWERS_AND_CARRY_FORWARD` | DOB helper required for birth-date answers and carry-forward |
156
+ | `DOB_QUESTION_TYPE_CHANGED` | DOB question type changed |
157
+ | `DUPLICATE_ADOPTION_TARGETS` | duplicate adoption targets |
158
+ | `DUPLICATE_CORRECTION_JOURNAL_KEYS` | duplicate correction journal keys |
159
+ | `DUPLICATE_HOUSEHOLD_JOURNAL_KEYS` | duplicate household journal keys |
160
+ | `DUPLICATE_HOUSEHOLD_MEMBER_ENROLLMENT` | duplicate household member enrollment |
161
+ | `DUPLICATE_HOUSEHOLD_REVIEW_IDS` | duplicate household review IDs |
162
+ | `DUPLICATE_JOURNAL_KEYS` | duplicate journal keys |
163
+ | `DUPLICATE_MATCHING_ENROLLMENTS` | duplicate matching enrollments |
164
+ | `DUPLICATE_MATCHING_SUBASSESSMENT_ROWS` | duplicate matching subassessment rows |
165
+ | `DUPLICATE_SEMANTIC_SUBASSESSMENT_ROWS` | duplicate semantic subassessment rows |
166
+ | `DUPLICATE_SUBASSESSMENT_KEY` | duplicate subassessment key |
167
+ | `EMPTY_ENTRY_CORRECTION` | empty entry correction |
168
+ | `EMPTY_ROW_CORRECTION` | empty row correction |
169
+ | `END_DATE_PRECEDES_RECORD_START` | end date precedes record start |
170
+ | `END_DATE_TARGET_INTERVAL_MISSING` | end-date target interval missing |
171
+ | `END_DATE_TARGET_MISSING` | end-date target missing |
172
+ | `ENROLLMENT_ALREADY_JOURNALED_USE_ITS_EXISTING_ENTRY_KEY` | enrollment already journaled; use its existing entry key |
173
+ | `ENROLLMENT_COUNT_UNAVAILABLE_OR_CHANGED` | enrollment count unavailable or changed |
174
+ | `ENROLLMENT_LIST_CONTEXT_MISMATCH` | enrollment list context mismatch |
175
+ | `ENROLLMENT_LIST_EXCEEDS_LIMIT` | enrollment list exceeds limit |
176
+ | `ENROLLMENT_NOT_GROUPED` | standalone enrollment uses adoptEnrollment |
177
+ | `ENROLLMENT_READ_FAILED` | enrollment read failed |
178
+ | `ENTRY_AND_INTERIM_ASSESSMENT_LOCATIONS_REQUIRED_EXIT_LOCATION_MUST_BE_NONEMPTY_WHEN_SUPPLIED` | entry and interim assessment locations required; exit location must be nonempty when supplied |
179
+ | `ENTRY_CHANGED_SINCE_CORRECTION_PREFLIGHT` | entry changed since correction preflight |
180
+ | `ENTRY_CORRECTION_DID_NOT_VERIFY` | entry correction did not verify |
181
+ | `ENTRY_CORRECTION_ENROLLMENT_CONTEXT_MISMATCH` | entry correction enrollment context mismatch |
182
+ | `ENTRY_CORRECTION_MUST_PRECEDE_INTERIM_OR_EXIT_WORKFLOWS` | entry correction must precede interim or exit workflows |
183
+ | `ENTRY_CORRECTION_REJECTED` | correction was resolved as not applied; use a new key for a corrected intent<br>entry correction rejected |
184
+ | `ENTRY_CORRECTION_REQUIRES_AN_OPEN_ENROLLMENT_WITHOUT_REVIEWS` | entry correction requires an open enrollment without reviews |
185
+ | `ENTRY_CORRECTION_UNCERTAIN` | entry correction uncertain<br>uncertain entry correction requires reconciliation |
186
+ | `EXISTING_EXIT_CONFLICTS_WITH_REQUESTED_EXIT` | existing exit conflicts with requested exit |
187
+ | `EXISTING_HOUSEHOLD_REVIEW_CONFLICT` | existing household review conflict |
188
+ | `EXIT_ASSESSMENT_CONFIGURATION_CHANGED` | exit assessment configuration changed |
189
+ | `EXIT_IDENTITY_CHANGED` | exit identity changed |
190
+ | `EXIT_IDENTITY_MISSING` | exit identity missing |
191
+ | `EXIT_PRECEDES_AN_EXISTING_REVIEW` | exit precedes an existing review |
192
+ | `EXIT_SUBGROUP_IDENTITY_CHANGED` | exit subgroup identity changed |
193
+ | `EXIT_SUBGROUP_MEMBERSHIP_MISMATCH` | exit subgroup membership mismatch |
194
+ | `EXIT_UI_AND_AUTHORITATIVE_ENROLLMENT_SESSION_REQUIRED` | exit UI and authoritative enrollment session required |
195
+ | `EXIT_UI_DESTINATION_AND_REASON_FOR_LEAVING_REQUIRED` | exit UI, destination and reason for leaving required |
196
+ | `EXPLICIT_ENROLLMENT_TARGETS_REQUIRED` | explicit enrollment targets required |
197
+ | `HEAD_AND_HOUSEHOLD_MEMBERS_REQUIRED` | head and household members required |
198
+ | `HEAD_CLIENT_ID_REQUIRED` | head client ID required |
199
+ | `HEAD_OF_HOUSEHOLD_ASSESSMENT_OPTION_UNRESOLVED` | head of household assessment option unresolved |
200
+ | `HEAD_OF_HOUSEHOLD_CHANGE_IS_A_RED_FLAG_USE_A_SEPARATE_RETROACTIVE_CORRECTION_FOR_A_MEMBER_WITH` | head of household change is a red flag; use a separate retroactive correction for a member with the same first entry date, or exit all enrollments and create a fresh enrollment |
201
+ | `HOUSEHOLD_API_AND_AUTHORITATIVE_ENROLLMENT_SESSION_REQUIRED` | household API and authoritative enrollment session required |
202
+ | `HOUSEHOLD_DISCOVERY_AMBIGUOUS` | a unique current household matching the full cohort, head and dates is required |
203
+ | `HOUSEHOLD_DISCOVERY_COHORT_CHANGED` | household membership changed during discovery<br>linked enrollment cohort changed during discovery |
204
+ | `HOUSEHOLD_DISCOVERY_DETAIL_INVALID` | complete linked enrollment cohort required<br>group flag conflicts with linked cohort |
205
+ | `HOUSEHOLD_DISCOVERY_INPUT_INVALID` | head client and enrollment IDs required |
206
+ | `HOUSEHOLD_DISCOVERY_LIST_INCOMPLETE` | household list count unavailable or changed<br>incomplete household list<br>invalid household list |
207
+ | `HOUSEHOLD_DISCOVERY_NOT_FOUND` | a unique current household matching the full cohort, head and dates is required |
208
+ | `HOUSEHOLD_DISCOVERY_READERS_REQUIRED` | GWT detail and complete household readers required |
209
+ | `HOUSEHOLD_ENROLLMENT_CONTEXT_MISMATCH` | household enrollment context mismatch |
210
+ | `HOUSEHOLD_ENROLLMENT_IDENTITY_CHANGED` | household enrollment identity changed |
211
+ | `HOUSEHOLD_ENROLLMENT_MEMBERS_CHANGED` | household enrollment members changed |
212
+ | `HOUSEHOLD_EXITS_ARE_NOT_LINKED` | household exits are not linked |
213
+ | `HOUSEHOLD_HEAD_MISMATCH_HEAD_CHANGES_ARE_A_RED_FLAG_AND_REQUIRE_SEPARATE_RETROACTIVE` | household head mismatch; head changes are a red flag and require separate retroactive correction or a fresh enrollment |
214
+ | `HOUSEHOLD_KEY_CONFLICTS_WITH_PREVIOUS_INTENT` | household key conflicts with previous intent |
215
+ | `HOUSEHOLD_MEMBERS_UNAVAILABLE` | household members unavailable |
216
+ | `HOUSEHOLD_MEMBERSHIP_CHANGED_WHILE_ACQUIRING_LOCKS_RETRY` | household membership changed while acquiring locks; retry |
217
+ | `HOUSEHOLD_ORIGINAL_HEAD_AND_COMPLETE_ENROLLMENT_COHORT_REQUIRED` | household, original head and complete enrollment cohort required |
218
+ | `HOUSEHOLD_PROGRAM_AND_SUPPORTED_ENTRY_EXIT_TYPE_REQUIRED` | household, program and supported Entry/Exit type required |
219
+ | `HOUSEHOLD_REVIEW_IDENTITY_CHANGED` | household review identity changed |
220
+ | `HOUSEHOLD_REVIEW_MEMBERSHIP_MISMATCH` | household review membership mismatch |
221
+ | `HOUSEHOLD_REVIEWS_UNAVAILABLE` | household reviews unavailable |
222
+ | `INCOMPLETE_ENROLLMENT_LIST` | incomplete enrollment list |
223
+ | `INCOMPLETE_EXISTING_EXIT_METADATA` | incomplete existing exit metadata |
224
+ | `INDEPENDENT_CONFIRMATION_REQUIRED` | independent non-application confirmation required |
225
+ | `INDIVIDUAL_ENROLLMENT_CONTEXT_MISMATCH` | individual enrollment context mismatch |
226
+ | `INDIVIDUAL_EXIT_MEMBERSHIP_MISMATCH` | individual exit membership mismatch |
227
+ | `INTERIM_DID_NOT_VERIFY` | interim did not verify |
228
+ | `INVALID_ASSESSMENT_PLAN` | invalid assessment plan |
229
+ | `INVALID_CORRECTION_CLIENT_OR_ROWS` | invalid correction client or rows |
230
+ | `INVALID_CORRECTION_DATE` | invalid correction date |
231
+ | `INVALID_CORRECTION_EFFECTIVE_DATE` | invalid correction effective date |
232
+ | `INVALID_ENROLLMENT_DATA_ADOPTED_MEMBERS_REQUIRE_MATCHING_PROGRAM_AND_HEAD_COVERAGE` | invalid enrollment data: adopted members require matching program and head coverage |
233
+ | `INVALID_ENROLLMENT_DATA_ENROLLMENT_HAS_NO_HEAD_OF_HOUSEHOLD` | invalid enrollment data: enrollment has no head of household |
234
+ | `INVALID_ENROLLMENT_DATA_HEAD_OF_HOUSEHOLD_RELATIONSHIP_IS_MISSING_OR_CLEARED` | invalid enrollment data: head of household relationship is missing or cleared |
235
+ | `INVALID_ENROLLMENT_DATA_HOUSEHOLD_HAS_NO_ACTIVE_HEAD_OF_HOUSEHOLD` | invalid enrollment data: household has no active head of household |
236
+ | `INVALID_ENROLLMENT_DATA_MEMBER_ENROLLMENT_WOULD_EXIST_WITHOUT_AN_ENROLLED_HEAD_OF_HOUSEHOLD` | invalid enrollment data: member enrollment would exist without an enrolled head of household; the original head must remain enrolled through every member exit; reconcile the data or exit all members and create a fresh enrollment |
237
+ | `INVALID_ENROLLMENT_LIST` | invalid enrollment list |
238
+ | `INVALID_HOUSEHOLD_ENROLLMENT_MEMBERS` | invalid household enrollment members |
239
+ | `INVALID_HOUSEHOLD_JOURNAL` | invalid household journal |
240
+ | `INVALID_OR_CONFLICTING_CORRECTION_ANSWERS_AND_CLEARS` | invalid or conflicting correction answers and clears |
241
+ | `INVALID_OR_DUPLICATE_END_DATE_TARGET` | invalid or duplicate end-date target |
242
+ | `INVALID_OR_REUSED_SUBASSESSMENT_ID` | invalid or reused subassessment ID |
243
+ | `INVALID_STORED_CORRECTION_DATE` | invalid stored correction date |
244
+ | `INVALID_STORED_CORRECTION_NUMBER` | invalid stored correction number |
245
+ | `INVALID_SUBASSESSMENT_PLAN` | invalid subassessment plan |
246
+ | `INVALID_WORKFLOW_JOURNAL` | invalid workflow journal |
247
+ | `KEY_CONFLICTS_WITH_PREVIOUS_INTENT` | key conflicts with previous intent |
248
+ | `LINKED_HOUSEHOLD_ENROLLMENT_REQUIRES_EXITHOUSEHOLD` | linked household enrollment requires exitHousehold |
249
+ | `LOOKUP_WRITE_LABEL_UNAVAILABLE` | lookup write label unavailable |
250
+ | `MANDATORY_CLEAR_CONFLICT` | mandatory clear conflict |
251
+ | `MANDATORY_CORRECTION_FIELD_COULD_NOT_BE_ENCODED` | mandatory correction field could not be encoded |
252
+ | `MANDATORY_CORRECTION_FIELD_REQUIRES_AN_EXPLICIT_ANSWER_OR_CLEAR` | mandatory correction field requires an explicit answer or clear |
253
+ | `MANDATORY_CORRECTION_LOOKUP_LABEL_UNAVAILABLE` | mandatory correction lookup label unavailable |
254
+ | `MATCHING_CORRECTION_JOURNAL_REQUIRED` | matching correction journal required |
255
+ | `MEMBER_ALREADY_HAS_AN_INDEPENDENT_MATCHING_ENROLLMENT` | member already has an independent matching enrollment |
256
+ | `MEMBER_CLIENT_AND_ASSESSMENT_REQUIRED` | member client and assessment required |
257
+ | `MEMBER_ENROLLMENT_HAS_NOT_BEEN_ATTACHED` | member enrollment has not been attached |
258
+ | `MEMBER_ENTRY_DATE_PORT_REQUIRED` | member entry date port required |
259
+ | `MEMBER_EXIT_IDENTITY_MISSING` | member exit identity missing |
260
+ | `MEMBER_IS_NOT_ACTIVE_IN_HOUSEHOLD_AT_ENTRY` | member is not active in household at entry |
261
+ | `MOBILE_ANSWER_EXISTS` | mobile answer exists<br>mobile scalar answer already exists at the effective date |
262
+ | `MOBILE_PREFLIGHT_UNAVAILABLE` | mobile preflight unavailable<br>mobile scalar preflight unavailable |
263
+ | `MULTIPLE_END_DATE_OVERRIDES` | multiple end-date overrides |
264
+ | `NEW_MEMBER_MUST_REMAIN_ENROLLED` | new member must remain enrolled |
265
+ | `NOT_APPLICABLE_EXPLANATION_REQUIRED` | not-applicable explanation required |
266
+ | `OPEN_HOUSEHOLD_ENROLLMENT_REQUIRED_TO_ADD_A_MEMBER` | open household enrollment required to add a member |
267
+ | `PARTIAL_EXISTING_HOUSEHOLD_EXIT_REQUIRES_RECONCILIATION` | partial existing household exit requires reconciliation |
268
+ | `PARTIAL_HOUSEHOLD_REVIEW_REQUIRES_RECONCILIATION` | partial household review requires reconciliation |
269
+ | `PICKLIST_CATALOG_CONTEXT_MISMATCH` | picklist catalog client mismatch<br>picklist catalog context mismatch |
270
+ | `PROGRAM_AND_SUPPORTED_ENTRY_EXIT_TYPE_REQUIRED` | program and supported Entry/Exit type required |
271
+ | `PROGRAM_ASSESSMENT_CONFIGURATION_CHANGED` | program assessment configuration changed |
272
+ | `PROGRAM_ASSESSMENT_DEFAULT_UNRESOLVED` | program assessment default unresolved |
273
+ | `PROVIDER_DISCOVERY_MISMATCH` | provider discovery mismatch |
274
+ | `RECORD_ALREADY_HAS_A_DIFFERENT_END_DATE` | record already has a different end date |
275
+ | `RESOLUTION_EVIDENCE_REFERENCE_CHANGED` | resolution evidence reference changed |
276
+ | `REST_API_DISCOVERY_ENROLLMENT_HELPERS_AND_DURABLE_STORE_REQUIRED` | REST API, discovery, enrollment helpers and durable store required |
277
+ | `REVIEW_FOLLOWS_PROJECT_EXIT` | review follows project exit |
278
+ | `REVIEW_PRECEDES_ENTRY` | review precedes entry |
279
+ | `ROW_CLOSURE_MUST_FALL_BETWEEN_ENTRY_AND_ASSESSMENT` | row closure must fall between entry and assessment |
280
+ | `ROW_REGISTRATION_BINDING_CONFLICT` | row key or recordset is already bound differently |
281
+ | `ROW_REGISTRATION_CONTEXT_MISMATCH` | enrollment identity changed during row registration |
282
+ | `ROW_REGISTRATION_ENTRY_REQUIRED` | verified enrollment workflow required |
283
+ | `ROW_REGISTRATION_INPUT_INVALID` | unique row keys, parents and positive recordset IDs required<br>valid registration key, entry key, client and explicit rows required |
284
+ | `ROW_REGISTRATION_INTENT_CONFLICT` | registration key conflicts with previous intent |
285
+ | `ROW_REGISTRATION_PARENT_INVALID` | subassessment parent not in entry definition |
286
+ | `ROW_REGISTRATION_READ_UNAVAILABLE` | complete row registration read required |
287
+ | `ROW_REGISTRATION_READER_REQUIRED` | authoritative enrollment reader required |
288
+ | `ROW_REGISTRATION_ROW_MISSING` | selected row is missing or ambiguous at the entry date or current read |
289
+ | `SAVED_ENROLLMENT_MISSING` | saved enrollment missing |
290
+ | `SAVED_ENROLLMENT_MISSING_OR_CHANGED` | saved enrollment missing or changed |
291
+ | `SESSION_INTERRUPTED` | session interrupted<br>upstream workflow operation failed |
292
+ | `STABLE_CORRECTION_KEY_REQUIRED` | stable correction key required |
293
+ | `STABLE_KEY_REQUIRED` | stable key required |
294
+ | `STANDARD_ASSESSMENT_WRITES_AND_AUTHORITATIVE_ENROLLMENT_READER_REQUIRED_FOR_CORRECTIONS` | standard assessment writes and authoritative enrollment reader required for corrections |
295
+ | `STORED_EXIT_DETAILS_CONFLICT_WITH_REQUESTED_EXIT` | stored exit details conflict with requested exit |
296
+ | `SUBASSESSMENT_READ_MISSING` | subassessment read missing |
297
+ | `TIMEZONE_QUALIFIED_WHOLE_SECOND_DATE_REQUIRED` | timezone-qualified whole-second date required |
298
+ | `UNCERTAIN_ENTRY_CORRECTION_REQUIRES_RECONCILIATION` | uncertain entry correction requires reconciliation |
299
+ | `UNCERTAIN_HOUSEHOLD_EXIT_REQUIRES_RECONCILIATION` | uncertain household exit requires reconciliation |
300
+ | `UNCERTAIN_MEMBER_ENTRY_DATE_REQUIRES_RECONCILIATION` | uncertain member entry date requires reconciliation |
301
+ | `UNCERTAIN_WRITE_REQUIRES_RECONCILIATION` | uncertain write requires reconciliation |
302
+ | `UNFINISHED_HOUSEHOLD_WORKFLOW_MUST_BE_RECONCILED_FIRST` | unfinished household workflow must be reconciled first |
303
+ | `UNIQUE_MEMBERS_INCLUDING_HEAD_REQUIRED` | unique members including head required |
304
+ | `UNKNOWN_CARRY_FORWARD_FIELD` | unknown carry-forward field |
305
+ | `UNKNOWN_OR_INVALID_CORRECTION_FIELDS` | unknown or invalid correction fields |
306
+ | `UNKNOWN_OR_INVALID_INPUT_FIELDS` | unknown or invalid input fields |
307
+ | `UNSELECTED_HOUSEHOLD_MEMBER_CHANGED` | unselected household member changed |
308
+ | `UNSELECTED_MEMBER_EXIT_IDENTITY_MISSING` | unselected member exit identity missing |
309
+ | `UNSUPPORTED_CORRECTION_FIELD_TYPE` | unsupported correction field type |
310
+ | `UNSUPPORTED_ENTRY_EXIT_TYPE_IN_ASSESSMENT_LOCATIONS` | unsupported Entry/Exit type in assessment locations |
311
+ | `UNSUPPORTED_MANDATORY_CORRECTION_FIELD` | unsupported mandatory correction field |
312
+ | `UNSUPPORTED_MANDATORY_FIELD_TYPE` | unsupported mandatory field type |
313
+ | `UNSUPPORTED_YES_NO_CLEAR` | unsupported yes no clear<br>yes/no answers cannot be cleared |
314
+ | `VERIFIED_CORRECTION_CANNOT_BE_REJECTED` | verified correction cannot be rejected |
315
+ | `VERIFIED_ENROLLMENT_WORKFLOW_REQUIRED` | verified enrollment workflow required |
316
+ | `VERIFIED_ENTRY_CORRECTION_CHANGED` | verified entry correction changed |
317
+ | `VERIFIED_EXIT_MISSING` | verified exit missing |
318
+ | `VERIFIED_HOUSEHOLD_ENTRY_REQUIRED` | verified household entry required |
319
+ | `VERIFIED_HOUSEHOLD_ENTRY_WITH_IDENTICAL_MEMBERS_REQUIRED_UNKNOWN_SELECTED_MEMBER` | verified household entry with identical members required; unknown selected member |
320
+ | `VERIFIED_ROW_CORRECTION_CHANGED` | verified row correction changed |
321
+ | `VERIFIED_STEP_CHANGED` | verified step changed |
322
+ | `WORKFLOW_CONFIGURATION_INVALID` | invalid workflow configuration<br>workflow configuration invalid |
323
+ | `WORKFLOW_FAILED` | Unclassified workflow failure; inspect cause and commit markers. |
324
+ | `WORKFLOW_JOURNAL_INVALID` | invalid workflow journal |
325
+ | `WORKFLOW_PENDING` | unfinished household workflow must be reconciled first<br>unfinished workflow must be reconciled first |
326
+ | `WORKFLOW_SCHEMA_CHANGED` | program assessment configuration changed |
327
+ | `WORKFLOW_UPSTREAM_FAILURE` | upstream workflow operation failed<br>workflow upstream failure |
328
+ | `WORKFLOW_VALIDATION_FAILED` | upstream workflow operation failed<br>workflow validation failed |
329
+ | `YES_NO_ANSWERS_CANNOT_BE_CLEARED` | yes/no answers cannot be cleared |
330
+ | `YES_NO_WRITE_MODE_CHANGED_SINCE_THIS_WORKFLOW_STARTED` | yes/no write mode changed since this workflow started |
331
+ | `YESNOWRITES_MUST_BE_MOBILE_OR_STANDARD_WITH_WRITELABEL_AND_ASSESSMENTS_CREATE` | yesNoWrites must be mobile, or standard with writeLabel and assessments.create |
package/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.8.1 - 2026-11-11
4
+
5
+ - Recheck mobile scalar collisions immediately before the first scalar-stage attempt, including answers that appeared after enrollment or review creation. Refuse known collisions and unavailable evidence without sending the batch or marking that stage attempted; preserve earlier lifecycle writes and prior-attempt reconciliation.
6
+ - Give complete-cohort adoption, standalone adoption of grouped entries, and changed verified steps specific stable codes instead of `WORKFLOW_FAILED`.
7
+ - Generate the complete enrollment workflow error-code reference in `API.md`; fail generation for new constant refusal reasons without codes, including conditional branches.
8
+ - Clarify that household discovery verifies current membership, not the original historical association, and explicit IDs do not bypass active-membership validation.
9
+ - Record the additional reported adoption/interim/correction acceptance from GitLab issues #5–#7, and distinguish the issue #2 report from the successful follow-up batch controls. The upstream partial-commit cause remains unresolved; no score-specific batch splitting is introduced.
10
+
11
+ ## 0.8.0 - 2026-11-11
12
+
13
+ - Resolve full lookup write labels without existing answers using `enrollmentSession.readPicklistOptions` and `createCatalogWriteLabelResolver`.
14
+ - Discover labels automatically in enrollment workflows when the configured session supports catalog reads and no explicit `writeLabel` override is supplied, including corrections and mandatory carry-forward.
15
+ - Match option ID, picklist ID and code; preserve full names including HUD suffixes, and reject incomplete or mismatched catalogs before writes. Read explicit option IDs in bounded batches.
16
+ - Read-only training verification matched 1,288 distinct catalog options to Coordinated Entry metadata from three synthetic clients, including the shared enhanced yes/no list and living-situation options. Live profile dropdowns matched the DOB-quality catalog labels. No new assessment writes were performed.
17
+
3
18
  ## 0.7.0 - 2026-11-10
4
19
 
5
20
  - Add `createReadBackWriteLabelResolver` for exact observed lookup labels, including HUD suffixes. Unobserved options remain unresolved; no suffix is inferred from metadata.
@@ -1,5 +1,5 @@
1
1
  import type {DateOfBirth} from './date-of-birth.mjs';
2
- import type {Answers, AssessmentDefinition, EncodingOptions, GwtEnrollmentHelpers, Provider, WellSkyClient} from '../dist/esm/index.js';
2
+ import type {PicklistCatalogSnapshot, Answers, AssessmentDefinition, EncodingOptions, GwtEnrollmentHelpers, Provider, WellSkyClient} from '../dist/esm/index.js';
3
3
  import type {UiMutationStore,EnrollmentReader} from './index.mjs';
4
4
  import type {ProviderAssessmentItem, ProgramDiscovery} from './program-discovery.mjs';
5
5
  import type {HouseholdExitUi,HouseholdExitDetailInput} from './household-exit.mjs';
@@ -41,6 +41,8 @@ export interface EnrollmentDiscoveryInput {
41
41
  entryDate:string;
42
42
  }
43
43
  export interface EnrollmentWorkflowDiscovery {
44
+ /** Current exact label catalog when automatic browser discovery is available. */
45
+ picklistOptions?:PicklistCatalogSnapshot;
44
46
  provider:Provider;
45
47
  entry:{assessment:ProviderAssessmentItem;definition:AssessmentDefinition};
46
48
  interim:{assessment:ProviderAssessmentItem;definition:AssessmentDefinition};
@@ -254,7 +256,7 @@ export interface EnrollmentWorkflowOptions {
254
256
  * `standard` writes them through `assessments.create` with every mandatory answer, after the mobile scalars;
255
257
  * every mandatory question must be a lookup. An unfinished journal never reconciles in the other mode. */
256
258
  yesNoWrites?:'mobile'|'standard';
257
- /** Required for `yesNoWrites: 'standard'`: tenant-verified labels for mandatory lookup answers. */
259
+ /** Explicit tenant-verified override. When omitted, a configured readPicklistOptions reader supplies exact catalog labels automatically. */
258
260
  writeLabel?:EncodingOptions['writeLabel'];
259
261
  }
260
262
  export class EnrollmentWorkflowError extends Error {
@@ -6,7 +6,7 @@ import {createEntryRowRegistrar} from './entry-row-registration.mjs';
6
6
  import {createHouseholdEnrollmentDiscovery} from './household-enrollment-discovery.mjs';
7
7
  import {workflowErrorCode} from './workflow-error-codes.mjs';
8
8
  import {createHash} from 'node:crypto';
9
- import {readProgramCatalog,encodeAnswers,encodeNewSubassessmentRow,toItems,parseRestInstant} from '../dist/esm/index.js';
9
+ import {readProgramCatalog,flattenQuestions,createCatalogWriteLabelResolver,encodeAnswers,encodeNewSubassessmentRow,toItems,parseRestInstant} from '../dist/esm/index.js';
10
10
 
11
11
  const object=v=>v!==null&&typeof v==='object'&&!Array.isArray(v);
12
12
  const positive=v=>Number.isSafeInteger(v)&&v>0;
@@ -128,7 +128,7 @@ export function createEnrollmentWorkflow({api,discovery,helpers,store,enrollment
128
128
  }
129
129
  };
130
130
  if(typeof discovery?.getProviderAssessments!=='function'||typeof helpers?.createInterimUpdate!=='function'||!store)fail('REST API, discovery, enrollment helpers and durable store required');
131
- if(!['mobile','standard'].includes(yesNoWrites)||yesNoWrites==='standard'&&(typeof writeLabel!=='function'||typeof api.assessments?.create!=='function'))fail('yesNoWrites must be mobile, or standard with writeLabel and assessments.create');
131
+ if(!['mobile','standard'].includes(yesNoWrites)||yesNoWrites==='standard'&&(typeof writeLabel!=='function'&&typeof enrollmentSession?.readPicklistOptions!=='function'||typeof api.assessments?.create!=='function'))fail('yesNoWrites must be mobile, or standard with writeLabel and assessments.create');
132
132
  if(!object(assessmentLocations))fail('assessment locations must be a map by Entry/Exit type');
133
133
  for(const [type,locations]of Object.entries(assessmentLocations)){
134
134
  if(!entryExitTypes.includes(type))fail('unsupported Entry/Exit type in assessment locations');
@@ -149,7 +149,14 @@ export function createEnrollmentWorkflow({api,discovery,helpers,store,enrollment
149
149
  if(definition.tenant!==api.tenant||definition.assessmentCode!==assessment.code||definition.unresolvedPicklists.length)fail('assessment metadata incomplete');
150
150
  resolved[phase]={assessment,definition};
151
151
  }
152
- return {provider:providers[0],...resolved};
152
+ let picklistOptions;
153
+ if(writeLabel===undefined&&typeof enrollmentSession?.readPicklistOptions==='function'){
154
+ const optionIds=[...new Set(Object.values(resolved).flatMap(({definition})=>flattenQuestions(definition.questions).filter(({question})=>question.questionType==='lookup').flatMap(({question})=>question.options.map(o=>o.id))))];
155
+ picklistOptions=await enrollmentSession.readPicklistOptions({clientId:input.clientId,optionIds});
156
+ if(picklistOptions?.clientId!==input.clientId)fail('picklist catalog client mismatch',{code:'PICKLIST_CATALOG_CONTEXT_MISMATCH'});
157
+ for(const {definition}of Object.values(resolved))createCatalogWriteLabelResolver(definition,picklistOptions);
158
+ }
159
+ return {provider:providers[0],...resolved,...(picklistOptions?{picklistOptions}:{})};
153
160
  };
154
161
  const scope=clientId=>JSON.stringify([tenantScope,'enrollment-workflow',clientId]);
155
162
  const reservationScope=clientId=>JSON.stringify([tenantScope,'household-workflow-reservation',clientId]);
@@ -210,7 +217,8 @@ export function createEnrollmentWorkflow({api,discovery,helpers,store,enrollment
210
217
  const definition=discovered[kind].definition,code=definition.assessmentCode,at=kind==='entry'?input.entryDate:(input.exitDate??input.reviewDate);
211
218
  const exitSchemaHash=kind==='exit'?hash({assessment:discovered.exit.assessment,questions:definition.questions}):undefined;
212
219
  if(record&&kind==='exit'&&record.exitSchemaHash!==exitSchemaHash)fail('exit assessment configuration changed');
213
- const plan=compile(definition,input.assessment,{yesNoWrites,writeLabel});
220
+ const operationWriteLabel=writeLabel??(discovered.picklistOptions?createCatalogWriteLabelResolver(definition,discovered.picklistOptions):undefined);
221
+ const plan=compile(definition,input.assessment,{yesNoWrites,writeLabel:operationWriteLabel});
214
222
  const carriesBirthDate=plan.carry.includes('SVPPROFDOB'),hasBirthDate=plan.dateOfBirth!==undefined||carriesBirthDate;
215
223
  let carriedBirthDate;
216
224
  if(hasBirthDate){
@@ -243,20 +251,20 @@ export function createEnrollmentWorkflow({api,discovery,helpers,store,enrollment
243
251
  // assessment's own rows. Read at the later of now and the assessment instant.
244
252
  const readCurrent=async()=>questions(await api.assessments.get({path:{clientId:input.clientId,assessmentCode:code},query:{dateEffective:new Date(Math.max(Date.now(),Date.parse(at))).toISOString().slice(0,19)+'Z'}}));
245
253
  // A generic read at exactly the effective instant returns null for yes/no answers saved at
246
- // that instant; one second later returns them. Only the yes/no step needs this.
254
+ // that instant; one second later returns them. Scalar preflight and verification use this.
247
255
  const readSaved=async()=>questions(await api.assessments.get({path:{clientId:input.clientId,assessmentCode:code},query:{dateEffective:new Date(Date.parse(at)+1000).toISOString()}}));
248
- const initial=await read();
249
- if(!record&&plan.scalar.length){
250
- const saved=await readSaved();
256
+ const assertScalarInsertable=saved=>{
251
257
  const existing=plan.scalar.map(q=>({q,row:saved.find(r=>r.virt_field_name===q.virt_field_name)}));
252
- const complete=existing.every(({q,row})=>row&&sameAnswer(q,row.answer_value)&&restDate(row.date_effective)===restDate(at));
258
+ const complete=existing.every(({q,row})=>row&&row.question_type===q.question_type&&sameAnswer(q,row.answer_value)&&restDate(row.date_effective)===restDate(at));
253
259
  if(!complete){
254
260
  const unavailable=existing.filter(({q,row})=>!row||row.question_type!==q.question_type||row.date_effective!=null&&(!Number.isFinite(restDate(row.date_effective))||restDate(row.date_effective)>restDate(at))||row.date_effective==null&&row.answer_value!=null&&row.answer_value!=='').map(({q})=>q.virt_field_name);
255
- if(unavailable.length)fail('mobile scalar preflight unavailable',{code:'MOBILE_PREFLIGHT_UNAVAILABLE',fields:unavailable});
261
+ if(unavailable.length)fail('mobile scalar preflight unavailable',{code:'MOBILE_PREFLIGHT_UNAVAILABLE',step:'scalars',fields:unavailable});
256
262
  const collisions=existing.filter(({row})=>restDate(row.date_effective)===restDate(at)).map(({q})=>q.virt_field_name);
257
- if(collisions.length)fail('mobile scalar answer already exists at the effective date',{code:'MOBILE_ANSWER_EXISTS',fields:collisions});
263
+ if(collisions.length)fail('mobile scalar answer already exists at the effective date',{code:'MOBILE_ANSWER_EXISTS',step:'scalars',fields:collisions});
258
264
  }
259
- }
265
+ };
266
+ const initial=await read();
267
+ if(!record&&plan.scalar.length)assertScalarInsertable(await readSaved());
260
268
  if(expectedHead===input.clientId&&headQuestion)checkHeadAnswer(proposedHead?.answer_value??initial.find(q=>q.virt_field_name==='HUD_RELATIONTOHOH')?.answer_value);
261
269
  for(const field of plan.carry.filter(field=>field!=='SVPPROFDOB')){if(kind==='entry'&&record?.correctedScalars?.[field])continue;const q=initial.find(q=>q.virt_field_name===field);if(!q||(q.question_type==='sub_assessment'?!q.sub_assessment_answer_list?.length:!q.answer_value))fail('carry-forward field is empty');}
262
270
  // Resend mandatory answers with the standard yes/no write. A blank write must not hide a recorded value.
@@ -266,7 +274,7 @@ export function createEnrollmentWorkflow({api,discovery,helpers,store,enrollment
266
274
  const recorded=initial.find(q=>q.virt_field_name===m.virt_field_name)?.answer_value;
267
275
  if(m.carry){
268
276
  const question=definition.questions.find(q=>q.fieldName===m.virt_field_name),options=question.options.filter(o=>String(o.id)===String(recorded));
269
- const label=options.length===1?writeLabel(question,options[0]):undefined;
277
+ const label=options.length===1?operationWriteLabel?.(question,options[0]):undefined;
270
278
  if(!label)fail(`carried mandatory answer ${m.virt_field_name} has no verified write label`,{code:'LOOKUP_WRITE_LABEL_UNAVAILABLE',fields:[m.virt_field_name]});
271
279
  if(m.virt_field_name==='HUD_RELATIONTOHOH')checkHeadAnswer(recorded);
272
280
  return {question_id:m.question_id,question_type:m.question_type,virt_field_name:m.virt_field_name,answer_value:label,expected:String(recorded)};
@@ -362,7 +370,13 @@ export function createEnrollmentWorkflow({api,discovery,helpers,store,enrollment
362
370
  },()=>api.subassessments.setEndDate({clientId:input.clientId,assessmentCode:code,effectiveDate:at,parentField:end.parentField,recordsetId:end.recordsetId,endDateField:end.endDateField,endDate:end.endDate}));
363
371
  }
364
372
  if(plan.scalar.length)await step('scalars',async()=>{
365
- const source=kind==='entry'&&record.correctedScalars?await readSaved():await read();return {found:plan.scalar.every(original=>{const q=kind==='entry'?record.correctedScalars?.[original.virt_field_name]??original:original,r=source.find(r=>r.virt_field_name===q.virt_field_name);return r&&sameAnswer(q,r.answer_value)&&(q.answer_value===''||restDate(r.date_effective)===restDate(at));}),value:plan.scalar.map(q=>q.virt_field_name)};
373
+ const source=await readSaved();
374
+ const found=plan.scalar.every(original=>{const q=kind==='entry'?record.correctedScalars?.[original.virt_field_name]??original:original,r=source.find(r=>r.virt_field_name===q.virt_field_name);return r&&r.question_type===q.question_type&&sameAnswer(q,r.answer_value)&&(q.answer_value===''||restDate(r.date_effective)===restDate(at));});
375
+ // Enrollment/review creation or another writer can populate shared questions after
376
+ // initial preflight. Refuse a known collision before journaling this first attempt.
377
+ // Prior attempts remain reconciliation-only, even when only part of a batch saved.
378
+ if(!found&&!record.steps.scalars)assertScalarInsertable(source);
379
+ return {found,value:plan.scalar.map(q=>q.virt_field_name)};
366
380
  },()=>api.assessmentsMobile.addAnswer({path:{clientId:input.clientId,assessmentCode:code},query:{dateEffective:at},body:plan.scalar}));
367
381
  // Standard writes update existing dated rows and change only the answers sent.
368
382
  if(plan.yesNo.length){const body=[...plan.yesNo,...mandatory.map(({expected,...q})=>q)],expected=[...plan.yesNo,...mandatory.map(({expected,...q})=>({...q,answer_value:expected}))];
@@ -1,5 +1,5 @@
1
1
  import {createHash} from 'node:crypto';
2
- import {encodeAnswers,parseRestInstant,toItems} from '../dist/esm/index.js';
2
+ import {createCatalogWriteLabelResolver,encodeAnswers,parseRestInstant,toItems} from '../dist/esm/index.js';
3
3
 
4
4
  const object=v=>v!==null&&typeof v==='object'&&!Array.isArray(v);
5
5
  const canonical=v=>JSON.stringify(v,(_,x)=>object(x)?Object.fromEntries(Object.keys(x).sort().map(k=>[k,x[k]])):x);
@@ -24,7 +24,7 @@ export function createEntryCorrector({api,store,enrollmentSession,writeLabel,dis
24
24
  if(type==='int'||type==='money'){const n=Number(v);if(!Number.isFinite(n))fail('invalid stored correction number');return n;}
25
25
  return String(v);
26
26
  };
27
- const encode=(questions,answers,clear,endpoint)=>{
27
+ const encode=(questions,answers,clear,endpoint,writeLabel)=>{
28
28
  if(!object(answers)||!Array.isArray(clear)||new Set(clear).size!==clear.length||clear.some(f=>typeof f!=='string'||Object.hasOwn(answers,f)))fail('invalid or conflicting correction answers and clears');
29
29
  const index=new Map(questions.map(q=>[q.fieldName,q]));
30
30
  const unsupportedClears=clear.filter(field=>index.get(field)?.questionType==='yes_no');
@@ -94,7 +94,7 @@ export function createEntryCorrector({api,store,enrollmentSession,writeLabel,dis
94
94
  const discovered=await discover({clientId:input.clientId,providerId,type,entryDate:at});
95
95
  const schemaHash=hash({providerId:discovered.provider.providerId,entry:{assessment:discovered.entry.assessment,questions:discovered.entry.definition.questions},interim:{assessment:discovered.interim.assessment,questions:discovered.interim.definition.questions}});
96
96
  if(schemaHash!==entry.schemaHash||record&&schemaHash!==record.schemaHash)fail('program assessment configuration changed');
97
- const definition=discovered.entry.definition,code=definition.assessmentCode;
97
+ const definition=discovered.entry.definition,operationWriteLabel=writeLabel??(discovered.picklistOptions?createCatalogWriteLabelResolver(discovered.entry.definition,discovered.picklistOptions):undefined),code=definition.assessmentCode;
98
98
  const snapshot=async()=>{
99
99
  const raw=await readQuestions(input.clientId,code,readAt),current=await readQuestions(input.clientId,code,new Date(Math.max(Date.now(),parseRestInstant(readAt))).toISOString());
100
100
  const scalars={},rows={};
@@ -145,7 +145,7 @@ export function createEntryCorrector({api,store,enrollmentSession,writeLabel,dis
145
145
  if(!record){
146
146
  readOverrides(observed);
147
147
  const answers=input.answers??{},clear=input.clear??[];
148
- const scalarBody=encode(definition.questions,answers,clear,'standalone'),scalarExpected=expected(definition.questions,scalarBody,answers);
148
+ const scalarBody=encode(definition.questions,answers,clear,'standalone',operationWriteLabel),scalarExpected=expected(definition.questions,scalarBody,answers);
149
149
  // The standard endpoint requires every mandatory scalar, even if this patch leaves it unchanged.
150
150
  if(scalarBody.length)for(const q of definition.questions.filter(q=>q.mandatory&&!['label','blank','sub_assessment'].includes(q.questionType)&&!scalarBody.some(c=>c.virt_field_name===q.fieldName))){
151
151
  if(q.fieldName==='SVPPROFDOB'||!scalarTypes.has(q.questionType))fail('unsupported mandatory correction field');
@@ -153,10 +153,10 @@ export function createEntryCorrector({api,store,enrollmentSession,writeLabel,dis
153
153
  if(prior?.answer_value===null||prior?.answer_value===undefined||prior?.answer_value==='')fail('mandatory correction field requires an explicit answer or clear');
154
154
  let body;
155
155
  if(q.questionType==='lookup'){
156
- const options=q.options.filter(o=>String(o.id)===String(prior.answer_value)),label=options.length===1?writeLabel?.(q,options[0]):undefined;
156
+ const options=q.options.filter(o=>String(o.id)===String(prior.answer_value)),label=options.length===1?operationWriteLabel?.(q,options[0]):undefined;
157
157
  if(!label)fail('mandatory correction lookup label unavailable');
158
158
  body=[{question_id:q.questionId,question_type:q.questionType,virt_field_name:q.fieldName,answer_value:label}];
159
- }else body=encodeAnswers([q],{[q.fieldName]:{value:prior.answer_value}},{endpoint:'standalone',writeLabel});
159
+ }else body=encodeAnswers([q],{[q.fieldName]:{value:prior.answer_value}},{endpoint:'standalone',writeLabel:operationWriteLabel});
160
160
  if(body.length!==1)fail('mandatory correction field could not be encoded');scalarBody.push(body[0]);
161
161
  scalarExpected.push({...body[0],answer_value:prior.answer_value});
162
162
  }
@@ -165,7 +165,7 @@ export function createEntryCorrector({api,store,enrollmentSession,writeLabel,dis
165
165
  fields(patch,['key','parentField','answers','clear']);key(patch.key);
166
166
  const id=entry.rows[patch.key],parent=definition.questions.find(q=>q.fieldName===patch.parentField&&q.questionType==='sub_assessment');
167
167
  if(!positive(id)||ids.has(id)||!parent||!observed.rows[parent.fieldName]?.[id]||entry.rowParents?.[patch.key]&&entry.rowParents[patch.key]!==patch.parentField)fail('correction requires an existing entry row key and matching parent');ids.add(id);
168
- const cells=encode(parent.children,patch.answers??{},patch.clear??[],'mobile');if(!cells.length)fail('empty row correction');
168
+ const cells=encode(parent.children,patch.answers??{},patch.clear??[],'mobile',operationWriteLabel);if(!cells.length)fail('empty row correction');
169
169
  rowPlans.push({id,parentField:parent.fieldName,questionId:parent.questionId,cells});
170
170
  }
171
171
  if(!scalarBody.length&&!rowPlans.length)fail('empty entry correction');
@@ -1,6 +1,6 @@
1
1
  import type {UiConnection,UiConnectionOptions} from './connection.mjs';
2
2
  import type {Browser, BrowserContext, BrowserType, Page} from 'playwright-core';
3
- import type {GwtEnrollmentExit,GwtEnrollmentHelpers,GwtEnrollmentSession,GwtEnrollmentSnapshot,GwtMutationStore} from '../dist/esm/index.js';
3
+ import type {PicklistCatalogSnapshot,GwtEnrollmentExit,GwtEnrollmentHelpers,GwtEnrollmentSession,GwtEnrollmentSnapshot,GwtMutationStore} from '../dist/esm/index.js';
4
4
  export interface LoginOptions extends UiConnectionOptions {
5
5
  chromium: BrowserType;
6
6
  envFile?: string;
@@ -29,12 +29,15 @@ export interface EnrollmentListSnapshot {
29
29
  }
30
30
  export interface EnrollmentReader {
31
31
  readonly connection?:UiConnection;
32
+ /** Exact full names for explicit REST option IDs; does not read existing answers. */
33
+ readPicklistOptions?(target:{clientId:number;optionIds:readonly number[]}):Promise<PicklistCatalogSnapshot>;
32
34
  readEnrollment(target:{clientId:number;enrollmentId:number}):Promise<PlaywrightEnrollmentSnapshot>;
33
35
  /** Built-in sessions supply this complete, counted list, including exited entries.
34
36
  * Legacy custom readers without it retain REST list behavior. */
35
37
  listEnrollments?(target:{clientId:number}):Promise<EnrollmentListSnapshot>;
36
38
  }
37
39
  export interface EnrollmentCatalogReader extends EnrollmentReader {
40
+ readPicklistOptions(target:{clientId:number;optionIds:readonly number[]}):Promise<PicklistCatalogSnapshot>;
38
41
  listEnrollments(target:{clientId:number}):Promise<EnrollmentListSnapshot>;
39
42
  }
40
43
  export function openUiSession(options:LoginOptions):Promise<UiSession>;
package/browser/index.mjs CHANGED
@@ -10,7 +10,7 @@ export async function openEnrollmentSession({store,...loginOptions}) {
10
10
  try {
11
11
  const helpers=createPlaywrightEnrollmentHelpers({page:session.page,store,...session.connection});
12
12
  const reader=createPlaywrightGwtSession({page:session.page,...session.connection});
13
- const enrollmentSession={connection:session.connection,readEnrollment:target=>withPageLock(session.page,()=>reader.readEnrollment(target)),listEnrollments:target=>withPageLock(session.page,()=>reader.listEnrollments(target))};
13
+ const enrollmentSession={connection:session.connection,readEnrollment:target=>withPageLock(session.page,()=>reader.readEnrollment(target)),listEnrollments:target=>withPageLock(session.page,()=>reader.listEnrollments(target)),readPicklistOptions:target=>withPageLock(session.page,()=>reader.readPicklistOptions(target))};
14
14
  return Object.assign(session,{helpers,enrollmentSession,workflows:createClientWorkflows({page:session.page,store})});
15
15
  }catch(error){await session.close();throw error;}
16
16
  }
@@ -84,10 +84,10 @@ export function createPlaywrightGwtSession({page,tenant,moduleBaseUrl}) {
84
84
  try{const r=await fetch(url,{method:'POST',headers,body,credentials:'same-origin',redirect:'error',signal:controller.signal});return {status:r.status,body:await r.text()};}finally{clearTimeout(timer);}
85
85
  },request);
86
86
  };
87
- const mapped=async query=>{
87
+ const mapped=async(query,limit=1001)=>{
88
88
  if(!wire||!/^SELECT /.test(query)||/[|\\\r\n]/.test(query))throw Error('Invalid mapped read');
89
- const strings=[moduleBaseUrl,wire.policy,rpcInterface,'getMappedResults','java.lang.String/2004016611','java.util.HashMap/1797211028','I','Z',query+' LIMIT 1001,0'];
90
- const body=['7','0',strings.length,...strings,1,2,3,4,5,5,6,7,7,8,9,0,0,1001,1,''].join('|');
89
+ const strings=[moduleBaseUrl,wire.policy,rpcInterface,'getMappedResults','java.lang.String/2004016611','java.util.HashMap/1797211028','I','Z',query+` LIMIT ${limit},0`];
90
+ const body=['7','0',strings.length,...strings,1,2,3,4,5,5,6,7,7,8,9,0,0,limit,1,''].join('|');
91
91
  const response=await post({url:moduleBaseUrl+'service/MetaQueryService',headers:wire.headers,body});if(response.status!==200)throw Error('Mapped read failed');
92
92
  const data=decodeMappedResponse(response.body);if(data.total!==data.rows.length||data.total>1000)throw Error('Incomplete enrollment list');return data.rows;
93
93
  };
@@ -100,7 +100,7 @@ export function createPlaywrightGwtSession({page,tenant,moduleBaseUrl}) {
100
100
  if(ids.some(n=>!Number.isSafeInteger(n)||n<=0))throw Error('Invalid household tree');
101
101
  await closeDialogs();return [...new Set([target.clientId,...ids])].sort((a,b)=>a-b);
102
102
  };
103
- const listEnrollments=async({clientId})=>{
103
+ const openReadContext=async clientId=>{
104
104
  id(clientId);await guard();wire=undefined;let captured;
105
105
  const listener=request=>{
106
106
  if(request.url()===moduleBaseUrl+'service/MetaQueryService'&&request.postData()?.startsWith('7|0|')){
@@ -111,6 +111,9 @@ export function createPlaywrightGwtSession({page,tenant,moduleBaseUrl}) {
111
111
  page.on('request',listener);
112
112
  try{await openClient(clientId);wire=await captured;}finally{page.off('request',listener);}
113
113
  if(!wire?.policy||Object.values(wire.headers).some(v=>!v))throw Error('Fresh enrollment list context unavailable');
114
+ };
115
+ const listEnrollments=async({clientId})=>{
116
+ await openReadContext(clientId);
114
117
  const rows=await mapped(`SELECT e.entryExitId AS enrollmentId, e.clientId AS clientId, e.entryDate AS projectStartDate, e.exitDate AS projectExitDate, e.providerId AS enrollmentProvider, e.typeEntryExit.name AS type, e.groupId AS groupId FROM SpEntryExit AS e WHERE e.active = TRUE AND e.client.active = TRUE AND e.clientId = ${clientId} ORDER BY e.entryExitId`);
115
118
  const ids=new Set();
116
119
  for(const row of rows){
@@ -120,6 +123,18 @@ export function createPlaywrightGwtSession({page,tenant,moduleBaseUrl}) {
120
123
  }
121
124
  return {source:'gwt-rpc',tenant,clientId,count:rows.length,values:rows.map(row=>({...row,isActive:true,grouped:row.groupId!==null}))};
122
125
  };
126
+ const readPicklistOptions=async({clientId,optionIds})=>{
127
+ id(clientId);if(!Array.isArray(optionIds)||optionIds.length>10000)throw Error('Invalid picklist option IDs');
128
+ const ids=[...new Set(optionIds.map(id))].sort((a,b)=>a-b),values=[];
129
+ if(ids.length)await openReadContext(clientId);
130
+ for(let offset=0;offset<ids.length;offset+=200){
131
+ const chunk=ids.slice(offset,offset+200);
132
+ const rows=await mapped(`SELECT v.picklistValueId AS optionId, v.picklistId AS picklistId, v.name AS name, v.code AS code FROM SpPicklistValue AS v WHERE v.picklistValueId IN (${chunk.join(',')}) ORDER BY v.picklistValueId`,200);
133
+ if(rows.length!==chunk.length||new Set(rows.map(r=>r.optionId)).size!==chunk.length||rows.some(r=>!chunk.includes(r.optionId)||!Number.isSafeInteger(r.picklistId)||r.picklistId<=0||typeof r.name!=='string'||!r.name.trim()||r.code!==null&&typeof r.code!=='string'))throw Error('Incomplete picklist option catalog');
134
+ values.push(...rows);
135
+ }
136
+ return {source:'gwt-rpc',tenant,clientId,count:values.length,values};
137
+ };
123
138
  const readEnrollment=async target=>{
124
139
  const model=await openTarget(target),householdClientIds=await householdIds(target);
125
140
  const entries=await mapped(`SELECT e.entryExitId AS enrollmentId, e.clientId AS clientId, e.entryDate AS projectStartDate, e.groupId AS groupId, e.providerId AS providerId, e.typeEntryExit.name AS enrollmentType FROM SpEntryExit AS e WHERE e.active = TRUE AND e.entryExitId = ${id(target.enrollmentId)} AND e.clientId = ${id(target.clientId)}`);
@@ -213,7 +228,7 @@ export function createPlaywrightGwtSession({page,tenant,moduleBaseUrl}) {
213
228
  try{await save.click();const result=await Promise.race([captured,new Promise(resolve=>{timer=setTimeout(()=>resolve(null),15000);})]);if(!result)throw Error('Expected mutation was not captured');pending=digest(JSON.stringify({url:result.url,headers:result.headers,body:result.body}));return result;}
214
229
  finally{clearTimeout(timer);await page.unroute('**/service/**',handler);await ready();await closeDialogs();}
215
230
  };
216
- return {connection:resolveUiConnection({tenant,moduleBaseUrl}),readEnrollment,listEnrollments,prepare,async send(request){
231
+ return {connection:resolveUiConnection({tenant,moduleBaseUrl}),readEnrollment,listEnrollments,readPicklistOptions,prepare,async send(request){
217
232
  checkGeneration();const fingerprint=digest(JSON.stringify(request));if(!pending||fingerprint!==pending)throw Error('Only the newly prepared request can be sent once');pending=undefined;return post(request);
218
233
  }};
219
234
  }
@@ -1,5 +1,8 @@
1
1
  // Stable public codes. Keep code values when changing their associated messages.
2
2
  const codes = new Map([
3
+ ["adoption requires the complete linked enrollment cohort", "ADOPTION_COMPLETE_COHORT_REQUIRED"],
4
+ ["linked household enrollment requires adoptHouseholdEnrollment", "ADOPTION_HOUSEHOLD_REQUIRED"],
5
+ ["verified step changed", "VERIFIED_STEP_CHANGED"],
3
6
  ["DOB and household head corrections require their dedicated workflows", "DOB_AND_HOUSEHOLD_HEAD_CORRECTIONS_REQUIRE_THEIR_DEDICATED_WORKFLOWS"],
4
7
  ["DOB helper required for birth-date answers and carry-forward", "DOB_HELPER_REQUIRED_FOR_BIRTH_DATE_ANSWERS_AND_CARRY_FORWARD"],
5
8
  ["DOB question type changed", "DOB_QUESTION_TYPE_CHANGED"],
@@ -0,0 +1,22 @@
1
+ import type { AssessmentDefinition } from './metadata.js';
2
+ import type { EncodingOptions } from './encoding.js';
3
+ export interface PicklistCatalogOption {
4
+ optionId: number;
5
+ picklistId: number;
6
+ code: string | null;
7
+ name: string;
8
+ }
9
+ export interface PicklistCatalogSnapshot {
10
+ source: 'gwt-rpc';
11
+ tenant: string;
12
+ clientId: number;
13
+ count: number;
14
+ values: PicklistCatalogOption[];
15
+ }
16
+ export type CatalogWriteLabelResolver = NonNullable<EncodingOptions['writeLabel']> & {
17
+ readonly tenant: string;
18
+ };
19
+ /** Exact catalog names joined by identity, without reading any stored answer.
20
+ * Only choices present in the supplied assessment definition can resolve. */
21
+ export declare function createCatalogWriteLabelResolver(definition: AssessmentDefinition, catalog: PicklistCatalogSnapshot): CatalogWriteLabelResolver;
22
+ //# sourceMappingURL=picklist-labels.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"picklist-labels.d.ts","sourceRoot":"","sources":["../../src/picklist-labels.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,oBAAoB,EAAmC,MAAM,eAAe,CAAC;AAE1F,OAAO,KAAK,EAAC,eAAe,EAAC,MAAM,eAAe,CAAC;AAGnD,MAAM,WAAW,qBAAqB;IAAE,QAAQ,EAAC,MAAM,CAAC;IAAA,UAAU,EAAC,MAAM,CAAC;IAAA,IAAI,EAAC,MAAM,GAAC,IAAI,CAAC;IAAA,IAAI,EAAC,MAAM,CAAC;CAAC;AACxG,MAAM,WAAW,uBAAuB;IAAE,MAAM,EAAC,SAAS,CAAC;IAAA,MAAM,EAAC,MAAM,CAAC;IAAA,QAAQ,EAAC,MAAM,CAAC;IAAA,KAAK,EAAC,MAAM,CAAC;IAAA,MAAM,EAAC,qBAAqB,EAAE,CAAC;CAAC;AACtI,MAAM,MAAM,yBAAyB,GAAG,WAAW,CAAC,eAAe,CAAC,YAAY,CAAC,CAAC,GAAG;IAAC,QAAQ,CAAC,MAAM,EAAC,MAAM,CAAA;CAAC,CAAC;AAE9G;6EAC6E;AAC7E,wBAAgB,+BAA+B,CAAC,UAAU,EAAC,oBAAoB,EAAC,OAAO,EAAC,uBAAuB,GAAE,yBAAyB,CAoBzI"}
@@ -0,0 +1,35 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createCatalogWriteLabelResolver = createCatalogWriteLabelResolver;
4
+ const metadata_js_1 = require("./metadata.js");
5
+ const errors_js_1 = require("./errors.js");
6
+ /** Exact catalog names joined by identity, without reading any stored answer.
7
+ * Only choices present in the supplied assessment definition can resolve. */
8
+ function createCatalogWriteLabelResolver(definition, catalog) {
9
+ const fail = () => { throw new errors_js_1.ValidationError('incomplete or mismatched picklist label catalog'); };
10
+ if (catalog?.source !== 'gwt-rpc' || catalog.tenant !== definition.tenant || !Number.isSafeInteger(catalog.clientId) || catalog.clientId <= 0 || !Array.isArray(catalog.values) || catalog.count !== catalog.values.length)
11
+ fail();
12
+ const rows = new Map();
13
+ for (const row of catalog.values) {
14
+ if (!row || !Number.isSafeInteger(row.optionId) || row.optionId <= 0 || !Number.isSafeInteger(row.picklistId) || row.picklistId <= 0 || row.code !== null && typeof row.code !== 'string' || typeof row.name !== 'string' || !row.name.trim() || rows.has(row.optionId))
15
+ fail();
16
+ rows.set(row.optionId, { ...row });
17
+ }
18
+ const signature = (q, o) => JSON.stringify([q.questionId, q.fieldName, q.questionType, o.id, o.picklistId, o.code, o.picklistCode, o.name, o.hudVisibility]);
19
+ const labels = new Map();
20
+ for (const { question } of (0, metadata_js_1.flattenQuestions)(definition.questions)) {
21
+ if (question.questionType !== 'lookup')
22
+ continue;
23
+ for (const option of question.options) {
24
+ const row = rows.get(option.id);
25
+ if (!row || row.picklistId !== option.picklistId || row.code !== option.code) {
26
+ fail();
27
+ continue;
28
+ }
29
+ labels.set(signature(question, option), row.name);
30
+ }
31
+ }
32
+ const resolver = (question, option) => labels.get(signature(question, option));
33
+ return Object.freeze(Object.assign(resolver, { tenant: definition.tenant }));
34
+ }
35
+ //# sourceMappingURL=picklist-labels.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"picklist-labels.js","sourceRoot":"","sources":["../../src/picklist-labels.ts"],"names":[],"mappings":";;AAWA,0EAoBC;AA9BD,+CAA+C;AAE/C,2CAA4C;AAM5C;6EAC6E;AAC7E,SAAgB,+BAA+B,CAAC,UAA+B,EAAC,OAA+B;IAC9G,MAAM,IAAI,GAAC,GAAE,EAAE,GAAC,MAAM,IAAI,2BAAe,CAAC,iDAAiD,CAAC,CAAC,CAAA,CAAC,CAAC;IAC/F,IAAG,OAAO,EAAE,MAAM,KAAG,SAAS,IAAE,OAAO,CAAC,MAAM,KAAG,UAAU,CAAC,MAAM,IAAE,CAAC,MAAM,CAAC,aAAa,CAAC,OAAO,CAAC,QAAQ,CAAC,IAAE,OAAO,CAAC,QAAQ,IAAE,CAAC,IAAE,CAAC,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,IAAE,OAAO,CAAC,KAAK,KAAG,OAAO,CAAC,MAAM,CAAC,MAAM;QAAC,IAAI,EAAE,CAAC;IAC/M,MAAM,IAAI,GAAC,IAAI,GAAG,EAAgC,CAAC;IACnD,KAAI,MAAM,GAAG,IAAI,OAAO,CAAC,MAAM,EAAC,CAAC;QAChC,IAAG,CAAC,GAAG,IAAE,CAAC,MAAM,CAAC,aAAa,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAE,GAAG,CAAC,QAAQ,IAAE,CAAC,IAAE,CAAC,MAAM,CAAC,aAAa,CAAC,GAAG,CAAC,UAAU,CAAC,IAAE,GAAG,CAAC,UAAU,IAAE,CAAC,IAAE,GAAG,CAAC,IAAI,KAAG,IAAI,IAAE,OAAO,GAAG,CAAC,IAAI,KAAG,QAAQ,IAAE,OAAO,GAAG,CAAC,IAAI,KAAG,QAAQ,IAAE,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE,IAAE,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,QAAQ,CAAC;YAAC,IAAI,EAAE,CAAC;QAClP,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,QAAQ,EAAC,EAAC,GAAG,GAAG,EAAC,CAAC,CAAC;IACjC,CAAC;IACD,MAAM,SAAS,GAAC,CAAC,CAAoB,EAAC,CAAgB,EAAC,EAAE,CAAA,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,UAAU,EAAC,CAAC,CAAC,SAAS,EAAC,CAAC,CAAC,YAAY,EAAC,CAAC,CAAC,EAAE,EAAC,CAAC,CAAC,UAAU,EAAC,CAAC,CAAC,IAAI,EAAC,CAAC,CAAC,YAAY,EAAC,CAAC,CAAC,IAAI,EAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC;IAClL,MAAM,MAAM,GAAC,IAAI,GAAG,EAAiB,CAAC;IACtC,KAAI,MAAM,EAAC,QAAQ,EAAC,IAAG,IAAA,8BAAgB,EAAC,UAAU,CAAC,SAAS,CAAC,EAAC,CAAC;QAC9D,IAAG,QAAQ,CAAC,YAAY,KAAG,QAAQ;YAAC,SAAS;QAC7C,KAAI,MAAM,MAAM,IAAI,QAAQ,CAAC,OAAO,EAAC,CAAC;YACrC,MAAM,GAAG,GAAC,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;YAC9B,IAAG,CAAC,GAAG,IAAE,GAAG,CAAC,UAAU,KAAG,MAAM,CAAC,UAAU,IAAE,GAAG,CAAC,IAAI,KAAG,MAAM,CAAC,IAAI,EAAC,CAAC;gBAAA,IAAI,EAAE,CAAC;gBAAA,SAAS;YAAA,CAAC;YACtF,MAAM,CAAC,GAAG,CAAC,SAAS,CAAC,QAAQ,EAAC,MAAM,CAAC,EAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACjD,CAAC;IACF,CAAC;IACD,MAAM,QAAQ,GAAC,CAAC,QAA2B,EAAC,MAAqB,EAAC,EAAE,CAAA,MAAM,CAAC,GAAG,CAAC,SAAS,CAAC,QAAQ,EAAC,MAAM,CAAC,CAAC,CAAC;IAC3G,OAAO,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,QAAQ,EAAC,EAAC,MAAM,EAAC,UAAU,CAAC,MAAM,EAAC,CAAC,CAAC,CAAC;AAC1E,CAAC","sourcesContent":["import type {AssessmentDefinition,PicklistOption,QuestionDefinition} from './metadata.js';\nimport {flattenQuestions} from './metadata.js';\nimport type {EncodingOptions} from './encoding.js';\nimport {ValidationError} from './errors.js';\n\nexport interface PicklistCatalogOption {optionId:number;picklistId:number;code:string|null;name:string;}\nexport interface PicklistCatalogSnapshot {source:'gwt-rpc';tenant:string;clientId:number;count:number;values:PicklistCatalogOption[];}\nexport type CatalogWriteLabelResolver = NonNullable<EncodingOptions['writeLabel']> & {readonly tenant:string};\n\n/** Exact catalog names joined by identity, without reading any stored answer.\n * Only choices present in the supplied assessment definition can resolve. */\nexport function createCatalogWriteLabelResolver(definition:AssessmentDefinition,catalog:PicklistCatalogSnapshot):CatalogWriteLabelResolver{\n const fail=()=>{throw new ValidationError('incomplete or mismatched picklist label catalog');};\n if(catalog?.source!=='gwt-rpc'||catalog.tenant!==definition.tenant||!Number.isSafeInteger(catalog.clientId)||catalog.clientId<=0||!Array.isArray(catalog.values)||catalog.count!==catalog.values.length)fail();\n const rows=new Map<number,PicklistCatalogOption>();\n for(const row of catalog.values){\n if(!row||!Number.isSafeInteger(row.optionId)||row.optionId<=0||!Number.isSafeInteger(row.picklistId)||row.picklistId<=0||row.code!==null&&typeof row.code!=='string'||typeof row.name!=='string'||!row.name.trim()||rows.has(row.optionId))fail();\n rows.set(row.optionId,{...row});\n }\n const signature=(q:QuestionDefinition,o:PicklistOption)=>JSON.stringify([q.questionId,q.fieldName,q.questionType,o.id,o.picklistId,o.code,o.picklistCode,o.name,o.hudVisibility]);\n const labels=new Map<string,string>();\n for(const {question}of flattenQuestions(definition.questions)){\n if(question.questionType!=='lookup')continue;\n for(const option of question.options){\n const row=rows.get(option.id);\n if(!row||row.picklistId!==option.picklistId||row.code!==option.code){fail();continue;}\n labels.set(signature(question,option),row.name);\n }\n }\n const resolver=(question:QuestionDefinition,option:PicklistOption)=>labels.get(signature(question,option));\n return Object.freeze(Object.assign(resolver,{tenant:definition.tenant}));\n}\n"]}
package/dist/cjs/v2.d.ts CHANGED
@@ -12,4 +12,6 @@ export * from "./gwt-enrollments.js";
12
12
  export * from "./program-discovery.js";
13
13
  export * from "./mobile-preflight.js";
14
14
  export * from "./write-labels.js";
15
+ export { createCatalogWriteLabelResolver } from './picklist-labels.js';
16
+ export type { PicklistCatalogOption, PicklistCatalogSnapshot, CatalogWriteLabelResolver } from './picklist-labels.js';
15
17
  //# sourceMappingURL=v2.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"v2.d.ts","sourceRoot":"","sources":["../../src/v2.ts"],"names":[],"mappings":"AAAA,cAAc,mBAAmB,CAAC;AAClC,cAAc,0BAA0B,CAAC;AACzC,cAAc,cAAc,CAAC;AAC7B,cAAc,eAAe,CAAC;AAC9B,cAAc,aAAa,CAAC;AAC5B,cAAc,eAAe,CAAC;AAC9B,cAAc,mBAAmB,CAAC;AAClC,cAAc,WAAW,CAAC;AAC1B,cAAc,qBAAqB,CAAC;AACpC,cAAc,+BAA+B,CAAC;AAE9C,cAAc,sBAAsB,CAAC;AACrC,cAAc,wBAAwB,CAAC;AACvC,cAAc,uBAAuB,CAAC;AACtC,cAAc,mBAAmB,CAAC"}
1
+ {"version":3,"file":"v2.d.ts","sourceRoot":"","sources":["../../src/v2.ts"],"names":[],"mappings":"AAAA,cAAc,mBAAmB,CAAC;AAClC,cAAc,0BAA0B,CAAC;AACzC,cAAc,cAAc,CAAC;AAC7B,cAAc,eAAe,CAAC;AAC9B,cAAc,aAAa,CAAC;AAC5B,cAAc,eAAe,CAAC;AAC9B,cAAc,mBAAmB,CAAC;AAClC,cAAc,WAAW,CAAC;AAC1B,cAAc,qBAAqB,CAAC;AACpC,cAAc,+BAA+B,CAAC;AAE9C,cAAc,sBAAsB,CAAC;AACrC,cAAc,wBAAwB,CAAC;AACvC,cAAc,uBAAuB,CAAC;AACtC,cAAc,mBAAmB,CAAC;AAElC,OAAO,EAAC,+BAA+B,EAAC,MAAM,sBAAsB,CAAC;AACrE,YAAY,EAAC,qBAAqB,EAAC,uBAAuB,EAAC,yBAAyB,EAAC,MAAM,sBAAsB,CAAC"}
package/dist/cjs/v2.js CHANGED
@@ -14,6 +14,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
14
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.createCatalogWriteLabelResolver = void 0;
17
18
  __exportStar(require("./generated/v2.js"), exports);
18
19
  __exportStar(require("./generated/v2-models.js"), exports);
19
20
  __exportStar(require("./runtime.js"), exports);
@@ -28,4 +29,6 @@ __exportStar(require("./gwt-enrollments.js"), exports);
28
29
  __exportStar(require("./program-discovery.js"), exports);
29
30
  __exportStar(require("./mobile-preflight.js"), exports);
30
31
  __exportStar(require("./write-labels.js"), exports);
32
+ var picklist_labels_js_1 = require("./picklist-labels.js");
33
+ Object.defineProperty(exports, "createCatalogWriteLabelResolver", { enumerable: true, get: function () { return picklist_labels_js_1.createCatalogWriteLabelResolver; } });
31
34
  //# sourceMappingURL=v2.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"v2.js","sourceRoot":"","sources":["../../src/v2.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA,oDAAkC;AAClC,2DAAyC;AACzC,+CAA6B;AAC7B,gDAA8B;AAC9B,8CAA4B;AAC5B,gDAA8B;AAC9B,oDAAkC;AAClC,4CAA0B;AAC1B,sDAAoC;AACpC,gEAA8C;AAE9C,uDAAqC;AACrC,yDAAuC;AACvC,wDAAsC;AACtC,oDAAkC","sourcesContent":["export * from \"./generated/v2.js\";\nexport * from \"./generated/v2-models.js\";\nexport * from \"./runtime.js\";\nexport * from \"./metadata.js\";\nexport * from \"./errors.js\";\nexport * from \"./encoding.js\";\nexport * from \"./verification.js\";\nexport * from \"./bulk.js\";\nexport * from \"./subassessments.js\";\nexport * from \"./enrollment-verifications.js\";\n\nexport * from \"./gwt-enrollments.js\";\nexport * from \"./program-discovery.js\";\nexport * from \"./mobile-preflight.js\";\nexport * from \"./write-labels.js\";\n"]}
1
+ {"version":3,"file":"v2.js","sourceRoot":"","sources":["../../src/v2.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;AAAA,oDAAkC;AAClC,2DAAyC;AACzC,+CAA6B;AAC7B,gDAA8B;AAC9B,8CAA4B;AAC5B,gDAA8B;AAC9B,oDAAkC;AAClC,4CAA0B;AAC1B,sDAAoC;AACpC,gEAA8C;AAE9C,uDAAqC;AACrC,yDAAuC;AACvC,wDAAsC;AACtC,oDAAkC;AAElC,2DAAqE;AAA7D,qIAAA,+BAA+B,OAAA","sourcesContent":["export * from \"./generated/v2.js\";\nexport * from \"./generated/v2-models.js\";\nexport * from \"./runtime.js\";\nexport * from \"./metadata.js\";\nexport * from \"./errors.js\";\nexport * from \"./encoding.js\";\nexport * from \"./verification.js\";\nexport * from \"./bulk.js\";\nexport * from \"./subassessments.js\";\nexport * from \"./enrollment-verifications.js\";\n\nexport * from \"./gwt-enrollments.js\";\nexport * from \"./program-discovery.js\";\nexport * from \"./mobile-preflight.js\";\nexport * from \"./write-labels.js\";\n\nexport {createCatalogWriteLabelResolver} from './picklist-labels.js';\nexport type {PicklistCatalogOption,PicklistCatalogSnapshot,CatalogWriteLabelResolver} from './picklist-labels.js';\n"]}
package/dist/cjs/v3.d.ts CHANGED
@@ -12,4 +12,6 @@ export * from "./gwt-enrollments.js";
12
12
  export * from "./program-discovery.js";
13
13
  export * from "./mobile-preflight.js";
14
14
  export * from "./write-labels.js";
15
+ export { createCatalogWriteLabelResolver } from './picklist-labels.js';
16
+ export type { PicklistCatalogOption, PicklistCatalogSnapshot, CatalogWriteLabelResolver } from './picklist-labels.js';
15
17
  //# sourceMappingURL=v3.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"v3.d.ts","sourceRoot":"","sources":["../../src/v3.ts"],"names":[],"mappings":"AAAA,cAAc,mBAAmB,CAAC;AAClC,cAAc,0BAA0B,CAAC;AACzC,cAAc,cAAc,CAAC;AAC7B,cAAc,eAAe,CAAC;AAC9B,cAAc,aAAa,CAAC;AAC5B,cAAc,eAAe,CAAC;AAC9B,cAAc,mBAAmB,CAAC;AAClC,cAAc,WAAW,CAAC;AAC1B,cAAc,qBAAqB,CAAC;AACpC,cAAc,+BAA+B,CAAC;AAE9C,cAAc,sBAAsB,CAAC;AACrC,cAAc,wBAAwB,CAAC;AACvC,cAAc,uBAAuB,CAAC;AACtC,cAAc,mBAAmB,CAAC"}
1
+ {"version":3,"file":"v3.d.ts","sourceRoot":"","sources":["../../src/v3.ts"],"names":[],"mappings":"AAAA,cAAc,mBAAmB,CAAC;AAClC,cAAc,0BAA0B,CAAC;AACzC,cAAc,cAAc,CAAC;AAC7B,cAAc,eAAe,CAAC;AAC9B,cAAc,aAAa,CAAC;AAC5B,cAAc,eAAe,CAAC;AAC9B,cAAc,mBAAmB,CAAC;AAClC,cAAc,WAAW,CAAC;AAC1B,cAAc,qBAAqB,CAAC;AACpC,cAAc,+BAA+B,CAAC;AAE9C,cAAc,sBAAsB,CAAC;AACrC,cAAc,wBAAwB,CAAC;AACvC,cAAc,uBAAuB,CAAC;AACtC,cAAc,mBAAmB,CAAC;AAElC,OAAO,EAAC,+BAA+B,EAAC,MAAM,sBAAsB,CAAC;AACrE,YAAY,EAAC,qBAAqB,EAAC,uBAAuB,EAAC,yBAAyB,EAAC,MAAM,sBAAsB,CAAC"}
package/dist/cjs/v3.js CHANGED
@@ -14,6 +14,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
14
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.createCatalogWriteLabelResolver = void 0;
17
18
  __exportStar(require("./generated/v3.js"), exports);
18
19
  __exportStar(require("./generated/v3-models.js"), exports);
19
20
  __exportStar(require("./runtime.js"), exports);
@@ -28,4 +29,6 @@ __exportStar(require("./gwt-enrollments.js"), exports);
28
29
  __exportStar(require("./program-discovery.js"), exports);
29
30
  __exportStar(require("./mobile-preflight.js"), exports);
30
31
  __exportStar(require("./write-labels.js"), exports);
32
+ var picklist_labels_js_1 = require("./picklist-labels.js");
33
+ Object.defineProperty(exports, "createCatalogWriteLabelResolver", { enumerable: true, get: function () { return picklist_labels_js_1.createCatalogWriteLabelResolver; } });
31
34
  //# sourceMappingURL=v3.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"v3.js","sourceRoot":"","sources":["../../src/v3.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA,oDAAkC;AAClC,2DAAyC;AACzC,+CAA6B;AAC7B,gDAA8B;AAC9B,8CAA4B;AAC5B,gDAA8B;AAC9B,oDAAkC;AAClC,4CAA0B;AAC1B,sDAAoC;AACpC,gEAA8C;AAE9C,uDAAqC;AACrC,yDAAuC;AACvC,wDAAsC;AACtC,oDAAkC","sourcesContent":["export * from \"./generated/v3.js\";\nexport * from \"./generated/v3-models.js\";\nexport * from \"./runtime.js\";\nexport * from \"./metadata.js\";\nexport * from \"./errors.js\";\nexport * from \"./encoding.js\";\nexport * from \"./verification.js\";\nexport * from \"./bulk.js\";\nexport * from \"./subassessments.js\";\nexport * from \"./enrollment-verifications.js\";\n\nexport * from \"./gwt-enrollments.js\";\nexport * from \"./program-discovery.js\";\nexport * from \"./mobile-preflight.js\";\nexport * from \"./write-labels.js\";\n"]}
1
+ {"version":3,"file":"v3.js","sourceRoot":"","sources":["../../src/v3.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;AAAA,oDAAkC;AAClC,2DAAyC;AACzC,+CAA6B;AAC7B,gDAA8B;AAC9B,8CAA4B;AAC5B,gDAA8B;AAC9B,oDAAkC;AAClC,4CAA0B;AAC1B,sDAAoC;AACpC,gEAA8C;AAE9C,uDAAqC;AACrC,yDAAuC;AACvC,wDAAsC;AACtC,oDAAkC;AAElC,2DAAqE;AAA7D,qIAAA,+BAA+B,OAAA","sourcesContent":["export * from \"./generated/v3.js\";\nexport * from \"./generated/v3-models.js\";\nexport * from \"./runtime.js\";\nexport * from \"./metadata.js\";\nexport * from \"./errors.js\";\nexport * from \"./encoding.js\";\nexport * from \"./verification.js\";\nexport * from \"./bulk.js\";\nexport * from \"./subassessments.js\";\nexport * from \"./enrollment-verifications.js\";\n\nexport * from \"./gwt-enrollments.js\";\nexport * from \"./program-discovery.js\";\nexport * from \"./mobile-preflight.js\";\nexport * from \"./write-labels.js\";\n\nexport {createCatalogWriteLabelResolver} from './picklist-labels.js';\nexport type {PicklistCatalogOption,PicklistCatalogSnapshot,CatalogWriteLabelResolver} from './picklist-labels.js';\n"]}
@@ -0,0 +1,22 @@
1
+ import type { AssessmentDefinition } from './metadata.js';
2
+ import type { EncodingOptions } from './encoding.js';
3
+ export interface PicklistCatalogOption {
4
+ optionId: number;
5
+ picklistId: number;
6
+ code: string | null;
7
+ name: string;
8
+ }
9
+ export interface PicklistCatalogSnapshot {
10
+ source: 'gwt-rpc';
11
+ tenant: string;
12
+ clientId: number;
13
+ count: number;
14
+ values: PicklistCatalogOption[];
15
+ }
16
+ export type CatalogWriteLabelResolver = NonNullable<EncodingOptions['writeLabel']> & {
17
+ readonly tenant: string;
18
+ };
19
+ /** Exact catalog names joined by identity, without reading any stored answer.
20
+ * Only choices present in the supplied assessment definition can resolve. */
21
+ export declare function createCatalogWriteLabelResolver(definition: AssessmentDefinition, catalog: PicklistCatalogSnapshot): CatalogWriteLabelResolver;
22
+ //# sourceMappingURL=picklist-labels.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"picklist-labels.d.ts","sourceRoot":"","sources":["../../src/picklist-labels.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,oBAAoB,EAAmC,MAAM,eAAe,CAAC;AAE1F,OAAO,KAAK,EAAC,eAAe,EAAC,MAAM,eAAe,CAAC;AAGnD,MAAM,WAAW,qBAAqB;IAAE,QAAQ,EAAC,MAAM,CAAC;IAAA,UAAU,EAAC,MAAM,CAAC;IAAA,IAAI,EAAC,MAAM,GAAC,IAAI,CAAC;IAAA,IAAI,EAAC,MAAM,CAAC;CAAC;AACxG,MAAM,WAAW,uBAAuB;IAAE,MAAM,EAAC,SAAS,CAAC;IAAA,MAAM,EAAC,MAAM,CAAC;IAAA,QAAQ,EAAC,MAAM,CAAC;IAAA,KAAK,EAAC,MAAM,CAAC;IAAA,MAAM,EAAC,qBAAqB,EAAE,CAAC;CAAC;AACtI,MAAM,MAAM,yBAAyB,GAAG,WAAW,CAAC,eAAe,CAAC,YAAY,CAAC,CAAC,GAAG;IAAC,QAAQ,CAAC,MAAM,EAAC,MAAM,CAAA;CAAC,CAAC;AAE9G;6EAC6E;AAC7E,wBAAgB,+BAA+B,CAAC,UAAU,EAAC,oBAAoB,EAAC,OAAO,EAAC,uBAAuB,GAAE,yBAAyB,CAoBzI"}
@@ -0,0 +1,32 @@
1
+ import { flattenQuestions } from './metadata.js';
2
+ import { ValidationError } from './errors.js';
3
+ /** Exact catalog names joined by identity, without reading any stored answer.
4
+ * Only choices present in the supplied assessment definition can resolve. */
5
+ export function createCatalogWriteLabelResolver(definition, catalog) {
6
+ const fail = () => { throw new ValidationError('incomplete or mismatched picklist label catalog'); };
7
+ if (catalog?.source !== 'gwt-rpc' || catalog.tenant !== definition.tenant || !Number.isSafeInteger(catalog.clientId) || catalog.clientId <= 0 || !Array.isArray(catalog.values) || catalog.count !== catalog.values.length)
8
+ fail();
9
+ const rows = new Map();
10
+ for (const row of catalog.values) {
11
+ if (!row || !Number.isSafeInteger(row.optionId) || row.optionId <= 0 || !Number.isSafeInteger(row.picklistId) || row.picklistId <= 0 || row.code !== null && typeof row.code !== 'string' || typeof row.name !== 'string' || !row.name.trim() || rows.has(row.optionId))
12
+ fail();
13
+ rows.set(row.optionId, { ...row });
14
+ }
15
+ const signature = (q, o) => JSON.stringify([q.questionId, q.fieldName, q.questionType, o.id, o.picklistId, o.code, o.picklistCode, o.name, o.hudVisibility]);
16
+ const labels = new Map();
17
+ for (const { question } of flattenQuestions(definition.questions)) {
18
+ if (question.questionType !== 'lookup')
19
+ continue;
20
+ for (const option of question.options) {
21
+ const row = rows.get(option.id);
22
+ if (!row || row.picklistId !== option.picklistId || row.code !== option.code) {
23
+ fail();
24
+ continue;
25
+ }
26
+ labels.set(signature(question, option), row.name);
27
+ }
28
+ }
29
+ const resolver = (question, option) => labels.get(signature(question, option));
30
+ return Object.freeze(Object.assign(resolver, { tenant: definition.tenant }));
31
+ }
32
+ //# sourceMappingURL=picklist-labels.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"picklist-labels.js","sourceRoot":"","sources":["../../src/picklist-labels.ts"],"names":[],"mappings":"AACA,OAAO,EAAC,gBAAgB,EAAC,MAAM,eAAe,CAAC;AAE/C,OAAO,EAAC,eAAe,EAAC,MAAM,aAAa,CAAC;AAM5C;6EAC6E;AAC7E,MAAM,UAAU,+BAA+B,CAAC,UAA+B,EAAC,OAA+B;IAC9G,MAAM,IAAI,GAAC,GAAE,EAAE,GAAC,MAAM,IAAI,eAAe,CAAC,iDAAiD,CAAC,CAAC,CAAA,CAAC,CAAC;IAC/F,IAAG,OAAO,EAAE,MAAM,KAAG,SAAS,IAAE,OAAO,CAAC,MAAM,KAAG,UAAU,CAAC,MAAM,IAAE,CAAC,MAAM,CAAC,aAAa,CAAC,OAAO,CAAC,QAAQ,CAAC,IAAE,OAAO,CAAC,QAAQ,IAAE,CAAC,IAAE,CAAC,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,IAAE,OAAO,CAAC,KAAK,KAAG,OAAO,CAAC,MAAM,CAAC,MAAM;QAAC,IAAI,EAAE,CAAC;IAC/M,MAAM,IAAI,GAAC,IAAI,GAAG,EAAgC,CAAC;IACnD,KAAI,MAAM,GAAG,IAAI,OAAO,CAAC,MAAM,EAAC,CAAC;QAChC,IAAG,CAAC,GAAG,IAAE,CAAC,MAAM,CAAC,aAAa,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAE,GAAG,CAAC,QAAQ,IAAE,CAAC,IAAE,CAAC,MAAM,CAAC,aAAa,CAAC,GAAG,CAAC,UAAU,CAAC,IAAE,GAAG,CAAC,UAAU,IAAE,CAAC,IAAE,GAAG,CAAC,IAAI,KAAG,IAAI,IAAE,OAAO,GAAG,CAAC,IAAI,KAAG,QAAQ,IAAE,OAAO,GAAG,CAAC,IAAI,KAAG,QAAQ,IAAE,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE,IAAE,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,QAAQ,CAAC;YAAC,IAAI,EAAE,CAAC;QAClP,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,QAAQ,EAAC,EAAC,GAAG,GAAG,EAAC,CAAC,CAAC;IACjC,CAAC;IACD,MAAM,SAAS,GAAC,CAAC,CAAoB,EAAC,CAAgB,EAAC,EAAE,CAAA,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,UAAU,EAAC,CAAC,CAAC,SAAS,EAAC,CAAC,CAAC,YAAY,EAAC,CAAC,CAAC,EAAE,EAAC,CAAC,CAAC,UAAU,EAAC,CAAC,CAAC,IAAI,EAAC,CAAC,CAAC,YAAY,EAAC,CAAC,CAAC,IAAI,EAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC;IAClL,MAAM,MAAM,GAAC,IAAI,GAAG,EAAiB,CAAC;IACtC,KAAI,MAAM,EAAC,QAAQ,EAAC,IAAG,gBAAgB,CAAC,UAAU,CAAC,SAAS,CAAC,EAAC,CAAC;QAC9D,IAAG,QAAQ,CAAC,YAAY,KAAG,QAAQ;YAAC,SAAS;QAC7C,KAAI,MAAM,MAAM,IAAI,QAAQ,CAAC,OAAO,EAAC,CAAC;YACrC,MAAM,GAAG,GAAC,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;YAC9B,IAAG,CAAC,GAAG,IAAE,GAAG,CAAC,UAAU,KAAG,MAAM,CAAC,UAAU,IAAE,GAAG,CAAC,IAAI,KAAG,MAAM,CAAC,IAAI,EAAC,CAAC;gBAAA,IAAI,EAAE,CAAC;gBAAA,SAAS;YAAA,CAAC;YACtF,MAAM,CAAC,GAAG,CAAC,SAAS,CAAC,QAAQ,EAAC,MAAM,CAAC,EAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACjD,CAAC;IACF,CAAC;IACD,MAAM,QAAQ,GAAC,CAAC,QAA2B,EAAC,MAAqB,EAAC,EAAE,CAAA,MAAM,CAAC,GAAG,CAAC,SAAS,CAAC,QAAQ,EAAC,MAAM,CAAC,CAAC,CAAC;IAC3G,OAAO,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,QAAQ,EAAC,EAAC,MAAM,EAAC,UAAU,CAAC,MAAM,EAAC,CAAC,CAAC,CAAC;AAC1E,CAAC","sourcesContent":["import type {AssessmentDefinition,PicklistOption,QuestionDefinition} from './metadata.js';\nimport {flattenQuestions} from './metadata.js';\nimport type {EncodingOptions} from './encoding.js';\nimport {ValidationError} from './errors.js';\n\nexport interface PicklistCatalogOption {optionId:number;picklistId:number;code:string|null;name:string;}\nexport interface PicklistCatalogSnapshot {source:'gwt-rpc';tenant:string;clientId:number;count:number;values:PicklistCatalogOption[];}\nexport type CatalogWriteLabelResolver = NonNullable<EncodingOptions['writeLabel']> & {readonly tenant:string};\n\n/** Exact catalog names joined by identity, without reading any stored answer.\n * Only choices present in the supplied assessment definition can resolve. */\nexport function createCatalogWriteLabelResolver(definition:AssessmentDefinition,catalog:PicklistCatalogSnapshot):CatalogWriteLabelResolver{\n const fail=()=>{throw new ValidationError('incomplete or mismatched picklist label catalog');};\n if(catalog?.source!=='gwt-rpc'||catalog.tenant!==definition.tenant||!Number.isSafeInteger(catalog.clientId)||catalog.clientId<=0||!Array.isArray(catalog.values)||catalog.count!==catalog.values.length)fail();\n const rows=new Map<number,PicklistCatalogOption>();\n for(const row of catalog.values){\n if(!row||!Number.isSafeInteger(row.optionId)||row.optionId<=0||!Number.isSafeInteger(row.picklistId)||row.picklistId<=0||row.code!==null&&typeof row.code!=='string'||typeof row.name!=='string'||!row.name.trim()||rows.has(row.optionId))fail();\n rows.set(row.optionId,{...row});\n }\n const signature=(q:QuestionDefinition,o:PicklistOption)=>JSON.stringify([q.questionId,q.fieldName,q.questionType,o.id,o.picklistId,o.code,o.picklistCode,o.name,o.hudVisibility]);\n const labels=new Map<string,string>();\n for(const {question}of flattenQuestions(definition.questions)){\n if(question.questionType!=='lookup')continue;\n for(const option of question.options){\n const row=rows.get(option.id);\n if(!row||row.picklistId!==option.picklistId||row.code!==option.code){fail();continue;}\n labels.set(signature(question,option),row.name);\n }\n }\n const resolver=(question:QuestionDefinition,option:PicklistOption)=>labels.get(signature(question,option));\n return Object.freeze(Object.assign(resolver,{tenant:definition.tenant}));\n}\n"]}
package/dist/esm/v2.d.ts CHANGED
@@ -12,4 +12,6 @@ export * from "./gwt-enrollments.js";
12
12
  export * from "./program-discovery.js";
13
13
  export * from "./mobile-preflight.js";
14
14
  export * from "./write-labels.js";
15
+ export { createCatalogWriteLabelResolver } from './picklist-labels.js';
16
+ export type { PicklistCatalogOption, PicklistCatalogSnapshot, CatalogWriteLabelResolver } from './picklist-labels.js';
15
17
  //# sourceMappingURL=v2.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"v2.d.ts","sourceRoot":"","sources":["../../src/v2.ts"],"names":[],"mappings":"AAAA,cAAc,mBAAmB,CAAC;AAClC,cAAc,0BAA0B,CAAC;AACzC,cAAc,cAAc,CAAC;AAC7B,cAAc,eAAe,CAAC;AAC9B,cAAc,aAAa,CAAC;AAC5B,cAAc,eAAe,CAAC;AAC9B,cAAc,mBAAmB,CAAC;AAClC,cAAc,WAAW,CAAC;AAC1B,cAAc,qBAAqB,CAAC;AACpC,cAAc,+BAA+B,CAAC;AAE9C,cAAc,sBAAsB,CAAC;AACrC,cAAc,wBAAwB,CAAC;AACvC,cAAc,uBAAuB,CAAC;AACtC,cAAc,mBAAmB,CAAC"}
1
+ {"version":3,"file":"v2.d.ts","sourceRoot":"","sources":["../../src/v2.ts"],"names":[],"mappings":"AAAA,cAAc,mBAAmB,CAAC;AAClC,cAAc,0BAA0B,CAAC;AACzC,cAAc,cAAc,CAAC;AAC7B,cAAc,eAAe,CAAC;AAC9B,cAAc,aAAa,CAAC;AAC5B,cAAc,eAAe,CAAC;AAC9B,cAAc,mBAAmB,CAAC;AAClC,cAAc,WAAW,CAAC;AAC1B,cAAc,qBAAqB,CAAC;AACpC,cAAc,+BAA+B,CAAC;AAE9C,cAAc,sBAAsB,CAAC;AACrC,cAAc,wBAAwB,CAAC;AACvC,cAAc,uBAAuB,CAAC;AACtC,cAAc,mBAAmB,CAAC;AAElC,OAAO,EAAC,+BAA+B,EAAC,MAAM,sBAAsB,CAAC;AACrE,YAAY,EAAC,qBAAqB,EAAC,uBAAuB,EAAC,yBAAyB,EAAC,MAAM,sBAAsB,CAAC"}
package/dist/esm/v2.js CHANGED
@@ -12,4 +12,5 @@ export * from "./gwt-enrollments.js";
12
12
  export * from "./program-discovery.js";
13
13
  export * from "./mobile-preflight.js";
14
14
  export * from "./write-labels.js";
15
+ export { createCatalogWriteLabelResolver } from './picklist-labels.js';
15
16
  //# sourceMappingURL=v2.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"v2.js","sourceRoot":"","sources":["../../src/v2.ts"],"names":[],"mappings":"AAAA,cAAc,mBAAmB,CAAC;AAClC,cAAc,0BAA0B,CAAC;AACzC,cAAc,cAAc,CAAC;AAC7B,cAAc,eAAe,CAAC;AAC9B,cAAc,aAAa,CAAC;AAC5B,cAAc,eAAe,CAAC;AAC9B,cAAc,mBAAmB,CAAC;AAClC,cAAc,WAAW,CAAC;AAC1B,cAAc,qBAAqB,CAAC;AACpC,cAAc,+BAA+B,CAAC;AAE9C,cAAc,sBAAsB,CAAC;AACrC,cAAc,wBAAwB,CAAC;AACvC,cAAc,uBAAuB,CAAC;AACtC,cAAc,mBAAmB,CAAC","sourcesContent":["export * from \"./generated/v2.js\";\nexport * from \"./generated/v2-models.js\";\nexport * from \"./runtime.js\";\nexport * from \"./metadata.js\";\nexport * from \"./errors.js\";\nexport * from \"./encoding.js\";\nexport * from \"./verification.js\";\nexport * from \"./bulk.js\";\nexport * from \"./subassessments.js\";\nexport * from \"./enrollment-verifications.js\";\n\nexport * from \"./gwt-enrollments.js\";\nexport * from \"./program-discovery.js\";\nexport * from \"./mobile-preflight.js\";\nexport * from \"./write-labels.js\";\n"]}
1
+ {"version":3,"file":"v2.js","sourceRoot":"","sources":["../../src/v2.ts"],"names":[],"mappings":"AAAA,cAAc,mBAAmB,CAAC;AAClC,cAAc,0BAA0B,CAAC;AACzC,cAAc,cAAc,CAAC;AAC7B,cAAc,eAAe,CAAC;AAC9B,cAAc,aAAa,CAAC;AAC5B,cAAc,eAAe,CAAC;AAC9B,cAAc,mBAAmB,CAAC;AAClC,cAAc,WAAW,CAAC;AAC1B,cAAc,qBAAqB,CAAC;AACpC,cAAc,+BAA+B,CAAC;AAE9C,cAAc,sBAAsB,CAAC;AACrC,cAAc,wBAAwB,CAAC;AACvC,cAAc,uBAAuB,CAAC;AACtC,cAAc,mBAAmB,CAAC;AAElC,OAAO,EAAC,+BAA+B,EAAC,MAAM,sBAAsB,CAAC","sourcesContent":["export * from \"./generated/v2.js\";\nexport * from \"./generated/v2-models.js\";\nexport * from \"./runtime.js\";\nexport * from \"./metadata.js\";\nexport * from \"./errors.js\";\nexport * from \"./encoding.js\";\nexport * from \"./verification.js\";\nexport * from \"./bulk.js\";\nexport * from \"./subassessments.js\";\nexport * from \"./enrollment-verifications.js\";\n\nexport * from \"./gwt-enrollments.js\";\nexport * from \"./program-discovery.js\";\nexport * from \"./mobile-preflight.js\";\nexport * from \"./write-labels.js\";\n\nexport {createCatalogWriteLabelResolver} from './picklist-labels.js';\nexport type {PicklistCatalogOption,PicklistCatalogSnapshot,CatalogWriteLabelResolver} from './picklist-labels.js';\n"]}
package/dist/esm/v3.d.ts CHANGED
@@ -12,4 +12,6 @@ export * from "./gwt-enrollments.js";
12
12
  export * from "./program-discovery.js";
13
13
  export * from "./mobile-preflight.js";
14
14
  export * from "./write-labels.js";
15
+ export { createCatalogWriteLabelResolver } from './picklist-labels.js';
16
+ export type { PicklistCatalogOption, PicklistCatalogSnapshot, CatalogWriteLabelResolver } from './picklist-labels.js';
15
17
  //# sourceMappingURL=v3.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"v3.d.ts","sourceRoot":"","sources":["../../src/v3.ts"],"names":[],"mappings":"AAAA,cAAc,mBAAmB,CAAC;AAClC,cAAc,0BAA0B,CAAC;AACzC,cAAc,cAAc,CAAC;AAC7B,cAAc,eAAe,CAAC;AAC9B,cAAc,aAAa,CAAC;AAC5B,cAAc,eAAe,CAAC;AAC9B,cAAc,mBAAmB,CAAC;AAClC,cAAc,WAAW,CAAC;AAC1B,cAAc,qBAAqB,CAAC;AACpC,cAAc,+BAA+B,CAAC;AAE9C,cAAc,sBAAsB,CAAC;AACrC,cAAc,wBAAwB,CAAC;AACvC,cAAc,uBAAuB,CAAC;AACtC,cAAc,mBAAmB,CAAC"}
1
+ {"version":3,"file":"v3.d.ts","sourceRoot":"","sources":["../../src/v3.ts"],"names":[],"mappings":"AAAA,cAAc,mBAAmB,CAAC;AAClC,cAAc,0BAA0B,CAAC;AACzC,cAAc,cAAc,CAAC;AAC7B,cAAc,eAAe,CAAC;AAC9B,cAAc,aAAa,CAAC;AAC5B,cAAc,eAAe,CAAC;AAC9B,cAAc,mBAAmB,CAAC;AAClC,cAAc,WAAW,CAAC;AAC1B,cAAc,qBAAqB,CAAC;AACpC,cAAc,+BAA+B,CAAC;AAE9C,cAAc,sBAAsB,CAAC;AACrC,cAAc,wBAAwB,CAAC;AACvC,cAAc,uBAAuB,CAAC;AACtC,cAAc,mBAAmB,CAAC;AAElC,OAAO,EAAC,+BAA+B,EAAC,MAAM,sBAAsB,CAAC;AACrE,YAAY,EAAC,qBAAqB,EAAC,uBAAuB,EAAC,yBAAyB,EAAC,MAAM,sBAAsB,CAAC"}
package/dist/esm/v3.js CHANGED
@@ -12,4 +12,5 @@ export * from "./gwt-enrollments.js";
12
12
  export * from "./program-discovery.js";
13
13
  export * from "./mobile-preflight.js";
14
14
  export * from "./write-labels.js";
15
+ export { createCatalogWriteLabelResolver } from './picklist-labels.js';
15
16
  //# sourceMappingURL=v3.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"v3.js","sourceRoot":"","sources":["../../src/v3.ts"],"names":[],"mappings":"AAAA,cAAc,mBAAmB,CAAC;AAClC,cAAc,0BAA0B,CAAC;AACzC,cAAc,cAAc,CAAC;AAC7B,cAAc,eAAe,CAAC;AAC9B,cAAc,aAAa,CAAC;AAC5B,cAAc,eAAe,CAAC;AAC9B,cAAc,mBAAmB,CAAC;AAClC,cAAc,WAAW,CAAC;AAC1B,cAAc,qBAAqB,CAAC;AACpC,cAAc,+BAA+B,CAAC;AAE9C,cAAc,sBAAsB,CAAC;AACrC,cAAc,wBAAwB,CAAC;AACvC,cAAc,uBAAuB,CAAC;AACtC,cAAc,mBAAmB,CAAC","sourcesContent":["export * from \"./generated/v3.js\";\nexport * from \"./generated/v3-models.js\";\nexport * from \"./runtime.js\";\nexport * from \"./metadata.js\";\nexport * from \"./errors.js\";\nexport * from \"./encoding.js\";\nexport * from \"./verification.js\";\nexport * from \"./bulk.js\";\nexport * from \"./subassessments.js\";\nexport * from \"./enrollment-verifications.js\";\n\nexport * from \"./gwt-enrollments.js\";\nexport * from \"./program-discovery.js\";\nexport * from \"./mobile-preflight.js\";\nexport * from \"./write-labels.js\";\n"]}
1
+ {"version":3,"file":"v3.js","sourceRoot":"","sources":["../../src/v3.ts"],"names":[],"mappings":"AAAA,cAAc,mBAAmB,CAAC;AAClC,cAAc,0BAA0B,CAAC;AACzC,cAAc,cAAc,CAAC;AAC7B,cAAc,eAAe,CAAC;AAC9B,cAAc,aAAa,CAAC;AAC5B,cAAc,eAAe,CAAC;AAC9B,cAAc,mBAAmB,CAAC;AAClC,cAAc,WAAW,CAAC;AAC1B,cAAc,qBAAqB,CAAC;AACpC,cAAc,+BAA+B,CAAC;AAE9C,cAAc,sBAAsB,CAAC;AACrC,cAAc,wBAAwB,CAAC;AACvC,cAAc,uBAAuB,CAAC;AACtC,cAAc,mBAAmB,CAAC;AAElC,OAAO,EAAC,+BAA+B,EAAC,MAAM,sBAAsB,CAAC","sourcesContent":["export * from \"./generated/v3.js\";\nexport * from \"./generated/v3-models.js\";\nexport * from \"./runtime.js\";\nexport * from \"./metadata.js\";\nexport * from \"./errors.js\";\nexport * from \"./encoding.js\";\nexport * from \"./verification.js\";\nexport * from \"./bulk.js\";\nexport * from \"./subassessments.js\";\nexport * from \"./enrollment-verifications.js\";\n\nexport * from \"./gwt-enrollments.js\";\nexport * from \"./program-discovery.js\";\nexport * from \"./mobile-preflight.js\";\nexport * from \"./write-labels.js\";\n\nexport {createCatalogWriteLabelResolver} from './picklist-labels.js';\nexport type {PicklistCatalogOption,PicklistCatalogSnapshot,CatalogWriteLabelResolver} from './picklist-labels.js';\n"]}
@@ -29,7 +29,25 @@ The default metadata cache TTL is 60 seconds with 100 entries per client. Config
29
29
 
30
30
  WellSky requires a client context and known assessment code for these reads. There is no documented global assessment-list or standalone picklist-list endpoint. Mobile option metadata has no effective-date filter, so definitions explicitly identify its options as current even when the generic assessment read is dated. Metadata also does not provide a complete questionnaire branching engine.
31
31
 
32
- Use the raw `assessments.get`, `assessmentsMobile.getAnswers`, and `enrollments.getAssessment` methods for recorded answers. Use `encodeAnswers` and `encodeNewSubassessmentRow` for endpoint-specific encoding. Mobile lookups use option IDs; standalone/enrollment lookups require an explicit `writeLabel` resolver with tenant-verified labels. Raw option names are not assumed to be valid write labels. Some HUD-marked choices require `(HUD)` in the full write label even when metadata `name` omits it. Do not infer this from trailing whitespace or append the suffix to every option. Continue supplying tenant-verified labels until a label resolver has authoritative metadata; recorded `answer_value_text` is useful evidence for the corresponding option. Provider answers are refused for mobile writes because that endpoint can silently discard them. Answer-state code mappings and multi-value serialization are caller-supplied; a refusal is never converted to `No`.
32
+ Use the raw `assessments.get`, `assessmentsMobile.getAnswers`, and `enrollments.getAssessment` methods for recorded answers. Use `encodeAnswers` and `encodeNewSubassessmentRow` for endpoint-specific encoding. Mobile lookups use option IDs; standalone/enrollment lookups require an explicit `writeLabel` resolver with tenant-verified labels. Raw option names are not assumed to be valid write labels. Some HUD-marked choices require `(HUD)` in the full write label even when metadata `name` omits it. Do not infer this from trailing whitespace or append the suffix to every option. Use the catalog resolver below for exact labels without existing answers, or retain an explicitly verified callback; recorded `answer_value_text` also supplies evidence for observed options. Provider answers are refused for mobile writes because that endpoint can silently discard them. Answer-state code mappings and multi-value serialization are caller-supplied; a refusal is never converted to `No`.
33
+
34
+ ## Resolve lookup labels before an answer exists
35
+
36
+ The browser enrollment session exposes `readPicklistOptions({clientId, optionIds})`. It reads the current GWT picklist catalog and returns complete, counted rows with `optionId`, `picklistId`, `code`, and the exact full `name`. It needs an authenticated session and client context, but no existing enrollment or answer. Only explicit positive option IDs are queried, in batches of 200; missing, duplicated or malformed results stop discovery.
37
+
38
+ ```ts
39
+ import {flattenQuestions, createCatalogWriteLabelResolver} from '@choiceapps/communityservices-sdk';
40
+ const definition = await api.assessments.getDefinition({clientId, assessmentCode});
41
+ const optionIds = [...new Set(flattenQuestions(definition.questions)
42
+ .filter(({question}) => question.questionType === 'lookup')
43
+ .flatMap(({question}) => question.options.map(option => option.id)))];
44
+ const catalog = await session.enrollmentSession.readPicklistOptions({clientId, optionIds});
45
+ const writeLabel = createCatalogWriteLabelResolver(definition, catalog);
46
+ ```
47
+
48
+ The resolver matches option ID, picklist ID and code, preserves the full catalog name, and permits only options already allowed by the question definition. HUD and non-HUD choices have distinct IDs. It never appends a suffix, guesses from whitespace, or broadens the question's allowed list. Missing identity evidence or a tenant mismatch throws before encoding. The immutable resolver returns `undefined` if called with changed option metadata.
49
+
50
+ `createEnrollmentWorkflow` uses this catalog automatically when its `enrollmentSession` has `readPicklistOptions` and `writeLabel` is omitted. This covers standard entry/interim/exit batches, mandatory carry-forward and `correctEntry`. Explicit callbacks take precedence and skip automatic catalog reads. Legacy custom sessions without this reader still need a callback. Raw REST clients do not acquire UI access implicitly. See [the typed example](../examples/catalog-write-labels.mts) and [read-only evidence](vendor-issues/lookup-write-labels.md).
33
51
 
34
52
  ## Resolve exact lookup write labels from readback
35
53
 
@@ -55,7 +73,7 @@ Reproduced on November 2, 2026 in a sandbox tenant with synthetic clients:
55
73
 
56
74
  - **Mobile `AddAssessmentAnswer` does not store yes/no values.** Requests with `"Yes"`, `"1"`/`"0"`, and `"true"`/`"false"` returned success and created a row at the intended effective date, but its value read back empty through both the generic and mobile reads. An integrator's 30-answer entry batch showed the same result.
57
75
  - **Mobile writes are insert-only per question and effective date.** A second mobile write to any question that already has a row at that date returned HTTP 400 `An error occurred while saving the entity changes`. This affected yes/no and integer questions alike, so a mobile retry cannot repair an empty yes/no row.
58
- - **A failed mobile batch can still save part of its answers.** On November 2, 2026, an integrator's 15-answer batch returned HTTP 400 a few seconds after the client's household enrollment and entry answers were created. Afterwards one of the 15 answers was stored and the others were empty. Its error body was not captured, and the client's rows just before the send were not read. Later sends of the batch failed with the insert-only error above because that row existed. On November 3, the same byte-identical batch returned 200 and read back completely on fresh synthetic clients with no enrollment, an enrolled single adult, and an enrolled household head; batches with and without the score total question, and in either order, also succeeded. The cause is unknown; a race with enrollment creation is possible but unproven. Treat a failed mobile write as uncertain and reconcile it from a read, as the enrollment workflow does; never resend it.
76
+ - **A failed mobile batch can still save part of its answers.** An integrator's November 2, 2026 report describes a 15-answer batch returning HTTP 400 with only the score total saved. GitLab issue #2 reports a captured entity-save error and no prior rows; separate retained notes describe a send shortly after household enrollment and do not retain a before-write snapshot. These accounts do not establish the same pre-write state. Later sends of the batch failed with the insert-only error above because that row existed. On November 3, the same byte-identical batch returned 200 and read back completely on fresh synthetic clients with no enrollment, an enrolled single adult, and an enrolled household head; batches with and without the score total question, and in either order, also succeeded. The cause is unknown; a race with enrollment creation is possible but unproven. Treat a failed mobile write as uncertain and reconcile it from a read, as the enrollment workflow does; never resend it.
59
77
  - **Standard `assessments.create` stores `"Yes"`/`"No"`.** It rejects `"true"`/`"false"`. It updates existing rows at the same effective date, including empty yes/no rows created by mobile writes, and changed only the answers it was sent in a full before/after comparison.
60
78
  - **Every standard write must include every mandatory answer**, even one already recorded at that date; otherwise it returns `All mandatory answers are not provided`. A conditionally mandatory lookup that does not apply can be sent as `""`; it is stored empty at that date.
61
79
  - **Generic reads omit yes/no answers at exactly their effective instant.** `assessments.get` with `dateEffective` equal to the saved instant returns `null` for those yes/no answers while other types read normally. One second later returns the saved value. The standard create response has the same omission. Mobile reads return the value, but they have no date filter and show only the latest row, so a later record hides the date being checked.
@@ -83,7 +101,9 @@ await writeMobileAnswersChecked(api, {
83
101
 
84
102
  `MobileAnswerPreflightError` has a stable `code`, affected `fields`, and `committed: false`. A collision uses `MOBILE_ANSWER_EXISTS`; unavailable evidence uses `MOBILE_PREFLIGHT_UNAVAILABLE`. The helper checks scalars, including questions shared across instruments, but does not detect duplicate subassessment rows. It makes one write after a successful preflight and does not retry or journal it. An error from that write retains its original type and possible uncertain outcome. Reads and writes are not atomic against other users. Pass the original instant; the SDK transport applies any configured mobile date correction.
85
103
 
86
- New enrollment workflow plans also check scalar collisions before creating an enrollment or sending a mobile batch. Existing pending journal operations retain their reconciliation behavior.
104
+ Enrollment workflow plans check scalar collisions before creating an enrollment and again immediately before the first scalar batch. An answer can appear during enrollment/review creation or from another writer after the earlier read. If the complete requested scalar state already matches, the workflow verifies it without another insert. If only some rows occupy the instant, or their values conflict, it refuses before marking the scalar stage attempted. Unavailable evidence also blocks that first attempt.
105
+
106
+ If an earlier enrollment, review or exit stage was already attempted, this refusal still has `committed: 'unknown'` for the overall operation and `step: 'scalars'`. Keep the existing key and journal; the enrollment may already exist. Reconcile the stored answers before resuming. Previously attempted scalar stages retain their normal uncertain-write reconciliation and are never resent or split automatically. No read-before-write check eliminates changes after the last read.
87
107
 
88
108
  ## Race and ethnicity combinations
89
109
 
@@ -96,6 +96,12 @@ The integrator reports these training-tenant results on November 10 and 11, 2026
96
96
  - Three pending 0.5.0 corrections were refused without confirmation and resolved as `rejected` with confirmation.
97
97
  - Earlier 0.5.0 runs verified individual/household adoption followed by exit, household re-enrollment after exit, and path corrections in both directions for individuals and households, with no-write same-key repeats.
98
98
 
99
- The integrator has not tested household adoption followed by `createHouseholdInterim`, scalar correction on an adopted enrollment, or a new correction key after resolution. Synthetic tests do not replace that acceptance.
99
+ Further integrator reports in GitLab issues #5, #6 and #7 confirm three additional 0.6.0 paths on November 11: household adoption followed by `createHouseholdInterim` (a session interruption reauthenticated, and a same-key retry verified without a duplicate review); scalar-only correction on an adopted enrollment (same-key retry wrote nothing); and a new correction key after the original correction was resolved as not applied. These are reported live results, not an independent SDK-team rerun. Row corrections on adopted entries require the later `registerEntryRows` API.
100
100
 
101
- The 0.7.0 label resolver, group flags, household discovery, row registration, error codes and mobile collision preflight have local regression coverage, not new live tenant acceptance. Full write labels remain unresolved for unobserved options. Household discovery refuses ambiguous current-membership matches. Yes/no blanking remains unsupported; no alternative route has been verified.
101
+ The 0.7.0 label resolver, group flags, household discovery, row registration, error codes and mobile collision preflight have local regression coverage, not new live tenant acceptance. In 0.7.0, full write labels remained unresolved for unobserved options; the 0.8.0 catalog reader below removes that limitation when UI catalog access is available. Household discovery refuses ambiguous current-membership matches. Yes/no blanking remains unsupported; no alternative route has been verified.
102
+
103
+ ## Catalog label verification, November 11, 2026
104
+
105
+ Read-only training checks matched all 1,288 distinct option IDs in the Coordinated Entry instrument to current GWT catalog rows across three synthetic clients (4,089 question/option pairs). Catalog names differed from REST metadata names for 139 option occurrences in one definition. The five enhanced yes/no HUD questions shared a catalog; plain yes/no alternatives had separate option IDs. The visible profile DOB-quality dropdown matched the catalog's full labels. This establishes metadata identity and displayed labels without prior-answer evidence, not fresh acceptance of every standard write. Existing enrollment and answer records were left unchanged.
106
+
107
+ SDK 0.8.0 uses the catalog automatically for workflows with a built-in enrollment session. REST-only applications can supply a catalog resolver or retain their explicit callback. See [catalog evidence and limitations](vendor-issues/lookup-write-labels.md).
@@ -130,7 +130,7 @@ Every mandatory label is resolved before the first write. The `yesno` stage veri
130
130
 
131
131
  ## Correct an existing entry assessment
132
132
 
133
- `correctEntry` patches an entry at its original effective timestamp. Supply the verified `createEnrollment` key as `enrollmentKey` and a new stable `key` for each correction. Configure `enrollmentSession` and a `writeLabel` resolver for tenant-verified lookup labels. A correction requires an open enrollment with no interim reviews or exit workflows.
133
+ `correctEntry` patches an entry at its original effective timestamp. Supply the verified `createEnrollment` key as `enrollmentKey` and a new stable `key` for each correction. Configure `enrollmentSession`; built-in sessions discover exact lookup labels automatically. An explicit `writeLabel` callback overrides that discovery. A correction requires an open enrollment with no interim reviews or exit workflows.
134
134
 
135
135
  ```ts
136
136
  const corrected = await workflow.correctEntry({
@@ -234,6 +234,8 @@ const detail = await session.enrollmentSession.readEnrollment({
234
234
 
235
235
  Use the session returned by `openEnrollmentSession`. Lists include open and exited entries, validate complete counts, and refuse more than 1,000 records. List rows and details include `groupId` and `grouped`. The flag describes a stored group link, including a group with one participant; it is not a member count and requires no per-row detail reads. These fields are required on managed catalog rows and optional on custom detail-reader contracts for compatibility. `groupId` is never a household ID. For a verified household match from a head enrollment, use `discoverHouseholdEnrollment` below.
236
236
 
237
+ The same session also exposes `readPicklistOptions({clientId, optionIds})` for exact labels before answers exist. It uses fresh read context, validates counted results, and makes no record writes. Enrollment workflows use it automatically unless `writeLabel` is supplied. See [catalog lookup labels](assessments.md#resolve-lookup-labels-before-an-answer-exists).
238
+
237
239
  ## Individual enrollment exits
238
240
 
239
241
  Use `exitEnrollment` to end a standalone person's program enrollment. It requires a verified `createEnrollment` entry in the same durable store. Supply `enrollmentSession: session.enrollmentSession` and `exitUi: createHouseholdExitUi({page: session.page})` when constructing the workflow. The same exit UI port serves individual and household forms.
@@ -278,7 +280,7 @@ const adopted = await workflow.adoptHouseholdEnrollment({
278
280
  });
279
281
  ```
280
282
 
281
- Discovery reads the linked enrollment cohort through GWT and matches it against complete current household membership reads. It requires one household containing the cohort, the same head, and membership dates covering each entry. It verifies that household again before returning `householdSource: 'verified-membership'`. This is a verified membership match, not a native historical household pointer on the enrollment. Zero matches, multiple matches, incomplete reads or changed relationships fail without adopting. Historical households that no longer match require the existing explicit adoption inputs and their validation. No record is written to WellSky during discovery or adoption.
283
+ Discovery reads the linked enrollment cohort through GWT and matches it against complete current household membership reads. It requires one household containing the cohort, the same head, and membership dates covering each entry. It verifies that household again before returning `householdSource: 'verified-membership'`. This is a verified membership match, not a native historical household pointer on the enrollment. Zero matches, multiple matches, incomplete reads or changed relationships fail without adopting. A unique current match does not prove the enrollment's original historical household association. If that original association is required, verify it independently; the SDK does not expose a native historical pointer. Explicit adoption inputs still require active household membership, the matching head and valid dates. Known historical IDs cannot bypass those guards, so inactive or changed historical households remain unsupported. No record is written to WellSky during discovery or adoption.
282
284
 
283
285
  ### Register existing entry rows
284
286
 
package/docs/reference.md CHANGED
@@ -8,14 +8,14 @@ Only the package export paths below are supported import entry points. Declarati
8
8
 
9
9
  Import from `@choiceapps/communityservices-sdk`, `@choiceapps/communityservices-sdk/v3`, or `@choiceapps/communityservices-sdk/v2`. The root selects V3 and also exports the testing helpers. Each version supplies its own generated HTTP models and fixed version header.
10
10
 
11
- Runtime exports at the root: `MobileAnswerPreflightError`, `writeMobileAnswersChecked`, `createReadBackWriteLabelResolver`, `AmbiguousCommitError`, `AssessmentMetadata`, `AuthError`, `CooldownError`, `DefinitionDriftError`, `FakeControl`, `FakeWellSkyClient`, `GwtEnrollmentConflictError`, `GwtEnrollmentHelpers`, `SubassessmentEditor`, `TenantMismatchError`, `ThrottledError`, `UnknownPicklistValueError`, `UpstreamError`, `ValidationError`, `WellSkyClient`, `WellSkyError`, `apiVersion`, `buildAssessmentDefinition`, `compareDefinition`, `createFakeWellSkyClient`, `createInMemoryGwtMutationStore`, `createWellSkyClient`, `encodeAnswers`, `encodeNewSubassessmentRow`, `flattenQuestions`, `mapBulk`, `operationRoutes`, `parseRestInstant`, `readEnrollmentVerifications`, `readOutcome`, `readProgramCatalog`, `resolvePicklistOption`, `semanticHash`, `specSha256`, `toItems`, `verifyWrite`.
11
+ Runtime exports at the root: `MobileAnswerPreflightError`, `writeMobileAnswersChecked`, `createReadBackWriteLabelResolver`, `createCatalogWriteLabelResolver`, `AmbiguousCommitError`, `AssessmentMetadata`, `AuthError`, `CooldownError`, `DefinitionDriftError`, `FakeControl`, `FakeWellSkyClient`, `GwtEnrollmentConflictError`, `GwtEnrollmentHelpers`, `SubassessmentEditor`, `TenantMismatchError`, `ThrottledError`, `UnknownPicklistValueError`, `UpstreamError`, `ValidationError`, `WellSkyClient`, `WellSkyError`, `apiVersion`, `buildAssessmentDefinition`, `compareDefinition`, `createFakeWellSkyClient`, `createInMemoryGwtMutationStore`, `createWellSkyClient`, `encodeAnswers`, `encodeNewSubassessmentRow`, `flattenQuestions`, `mapBulk`, `operationRoutes`, `parseRestInstant`, `readEnrollmentVerifications`, `readOutcome`, `readProgramCatalog`, `resolvePicklistOption`, `semanticHash`, `specSha256`, `toItems`, `verifyWrite`.
12
12
 
13
13
  | Contract | Declaration | Guide |
14
14
  | --- | --- | --- |
15
15
  | HTTP clients and generated models | [V3](../dist/esm/generated/v3.d.ts), [V2](../dist/esm/generated/v2.d.ts) | [REST](rest-client.md), [API table](../API.md) |
16
16
  | Authentication, transport, responses and errors | [Runtime](../dist/esm/runtime.d.ts) | [Configuration](configuration.md), [recovery](troubleshooting.md) |
17
17
  | Metadata and picklists | [Metadata](../dist/esm/metadata.d.ts) | [Assessments](assessments.md) |
18
- | Checked mobile inserts and exact lookup labels | [Preflight](../dist/esm/mobile-preflight.d.ts), [labels](../dist/esm/write-labels.d.ts) | [Assessments](assessments.md) |
18
+ | Checked mobile inserts and exact lookup labels | [Preflight](../dist/esm/mobile-preflight.d.ts), [readback labels](../dist/esm/write-labels.d.ts), [catalog labels](../dist/esm/picklist-labels.d.ts) | [Assessments](assessments.md) |
19
19
  | Answer encoding | [Encoding](../dist/esm/encoding.d.ts) | [Dates and answers](dates-and-answers.md) |
20
20
  | Write verification | [Verification](../dist/esm/verification.d.ts) | [REST](rest-client.md) |
21
21
  | Program catalog | [Program discovery](../dist/esm/program-discovery.d.ts) | [Enrollments](enrollments.md) |
@@ -59,7 +59,7 @@ Enrollment workflow errors expose stable `code` values. Common actionable codes
59
59
  | `MOBILE_ANSWER_EXISTS` | A scalar row already occupies the requested mobile effective instant |
60
60
  | `MOBILE_PREFLIGHT_UNAVAILABLE` | The read cannot establish whether a scalar insert is safe |
61
61
 
62
- The [workflow code table](../browser/workflow-error-codes.mjs) maps existing reasons to codes; new guards supply explicit codes. `committed: false` identifies a pre-write refusal for the named operation. `'unknown'` requires journal/readback reconciliation. Resolution errors separately retain `originalCommitted: 'unknown'` for the original correction. HTTP status and causes are exposed where captured; they are not proof of non-application.
62
+ The complete [SDK-owned code inventory](../API.md#enrollment-workflow-errors) is generated from the guards and stable reason map. Generation rejects new constant refusal reasons that have no specific code, including conditional branches. `committed: false` identifies a pre-write refusal for the named operation. `'unknown'` requires journal/readback reconciliation. Resolution errors separately retain `originalCommitted: 'unknown'` for the original correction. HTTP status and causes are exposed where captured; they are not proof of non-application.
63
63
 
64
64
  Pending 0.5.0 corrections may have no saved `failure` kind or status. Keep your own original failure evidence for the confirmation callback. Do not repeat the write to recover missing diagnostics.
65
65
 
@@ -0,0 +1,13 @@
1
+ # Exact lookup write labels from the UI catalog
2
+
3
+ On November 11, 2026, read-only inspection of the training application's current GWT catalog established a way to discover full lookup labels before an answer exists.
4
+
5
+ REST assessment option metadata can omit `(HUD)` from `name`. The current UI's `SpPicklistValue.name` retains it. A mapped query returns `picklistValueId`, `picklistId`, `code` and `name` together. HUD and non-HUD enhanced yes/no choices are separate catalog records. `show_on_hud_nonhud_site` and trailing whitespace are not reliable substitutes for those identities.
6
+
7
+ The read used the application's observed `MetaQueryService` / `GenericService.getMappedResults` wire context and selected only explicit option IDs already enumerated by REST metadata. The [browser reader](../../browser/playwright-session.mjs) implements the same query with bounded batches and counted result validation. The [public resolver contract](../../dist/esm/picklist-labels.d.ts) joins IDs and codes while retaining the question's allowed choices.
8
+
9
+ Evidence: Coordinated Entry definitions for three synthetic clients matched all 1,288 distinct option IDs (4,089 question/option pairs). Full names differed from REST metadata for 139 occurrences in a definition. Disabling condition, domestic violence, non-cash benefits, health insurance and income-source questions shared the enhanced yes/no list. Living-situation and homelessness-history options were included. The visible profile DOB-quality dropdown matched its full catalog names, including all displayed options rather than only the selected answer.
10
+
11
+ No enrollment or assessment was saved during this inspection. No new REST writes were made to test acceptance of every returned label. Private captures, client identifiers and tenant-specific catalogs are excluded from the package. This internal GWT route is not part of the published REST Swagger; schema, permission, count or identity failures must stop discovery. Explicit application callbacks remain available for deployments without the catalog reader.
12
+
13
+ See [assessment integration](../assessments.md#resolve-lookup-labels-before-an-answer-exists) for usage.
@@ -36,8 +36,16 @@ After a successful standard write, the generic assessment read with `dateEffecti
36
36
 
37
37
  ## A failed mobile batch saved one of its answers
38
38
 
39
- Observed by an integrator on November 2, 2026, with synthetic records. A 15-answer `AddAssessmentAnswer` batch (1 `date`, 13 `lookup`, 1 `int`) was sent a few seconds after the client's household enrollment and entry-assessment answers were created. It returned HTTP 400; the error body was not captured. Afterwards the `int` answer was stored at the correct effective date and the other 14 were empty. The client's rows for this assessment were not read just before the send, so the absence of prior rows is inferred from the integration's own writes. Resends failed as in [the existing-row case](#mobile-writes-fail-for-an-existing-row).
39
+ GitLab issue #2 reports a November 2, 2026 synthetic-client test: a combined 15-answer batch (1 `date`, 13 `lookup`, 1 `int`) returned HTTP 400 with `An error occurred while saving the entity changes. See the inner exception for details.` Only the integer score answer was stored afterwards. The issue says no row existed beforehand, that the error body was captured, and that the non-score batch and score-only batch each succeeded separately.
40
40
 
41
- On November 3, the byte-identical batch returned 200 and read back completely on fresh synthetic clients without an enrollment, an enrolled single adult, and an enrolled household head. Splitting the batch, reordering it, or omitting the `int` answer made no difference. The failure has not been reproduced.
41
+ Separate retained run notes describe a batch sent shortly after household enrollment and entry answers, without a retained before-write assessment snapshot or raw error body. These accounts differ in their stated evidence. The SDK team cannot establish that they describe identical attempts or independently confirm their pre-write state. An existing answer shared by two instruments is one possible collision source, not an established explanation of the reported failure.
42
42
 
43
- **Ask WellSky:** Can a mobile write that returns HTTP 400 commit part of its batch? Is a write to an assessment assigned to the provider affected by work the server does after enrollment creation?
43
+ On November 3, the byte-identical batch returned 200 and read back completely on fresh synthetic clients without an enrollment, an enrolled single adult, and an enrolled household head. Splitting the batch, reordering it, or omitting the `int` answer made no difference in those successful controls. The original failure remains unreproduced in that follow-up. These controls do not disprove the original report or establish a score-specific serialization rule.
44
+
45
+ ## SDK guard for an observed collision after preflight
46
+
47
+ A November 11 synthetic regression demonstrated a narrower SDK defect: enrollment creation populated a scalar after the initial preflight; the scalar-stage read observed that occupied instant, but the workflow still submitted the full mobile batch. SDK 0.8.1 now refuses that first scalar attempt before marking it attempted. This protects against an observed collision and unavailable evidence after an earlier lifecycle stage. It does not establish the cause of issue #2 or make mobile batches atomic.
48
+
49
+ A separate synthetic partial-commit control returned HTTP 400 after saving only one answer. Its stage stayed pending, same-key retries sent nothing, and later row stages did not run. Preserve that uncertain-write behavior. Do not split, replay, change the effective date, or replace the key to work around an already-attempted batch.
50
+
51
+ **Ask WellSky:** Can a mobile write that returns HTTP 400 commit part of its batch? Is a write to an assessment assigned to the provider affected by work the server does after enrollment creation? Which server-side diagnostic can identify the failing question and database constraint?
@@ -15,6 +15,7 @@ Every file here ships with the package and imports the public `@choiceapps/commu
15
15
  | [program-discovery.ts](program-discovery.ts) | Program and VA-entry assessment discovery | Exported function; authenticated page required |
16
16
  | [enrollment-workflow.mts](enrollment-workflow.mts) | Single entry and interim assessment plans | Exported function; caller supplies plans and dependencies |
17
17
  | [individual-exit-workflow.mts](individual-exit-workflow.mts) | Standalone program enrollment exit and assessment | Verified entry key, enrollment reader, exit UI port and durable store required |
18
+ | [catalog-write-labels.mts](catalog-write-labels.mts) | Discover full lookup labels without existing answers; automatic workflow configuration | Authenticated enrollment session and current assessment definition required |
18
19
  | [checked-assessment-writes.mts](checked-assessment-writes.mts) | Exact readback lookup labels and checked mobile scalar inserts | Unobserved labels remain unresolved; preflight does not make writes atomic |
19
20
  | [enrollment-discovery-and-correction.mts](enrollment-discovery-and-correction.mts) | Discover/adopt from the head and bind existing correction rows | Explicit row IDs and unique verified household membership required |
20
21
  | [entry-correction-workflow.mts](entry-correction-workflow.mts) | Same-date scalar clears and existing row edits | Original entry/row keys, verified lookup labels and durable store required |
@@ -0,0 +1,15 @@
1
+ import {createCatalogWriteLabelResolver,flattenQuestions,type WellSkyClient} from '@choiceapps/communityservices-sdk';
2
+ import {createEnrollmentWorkflow,type EnrollmentCatalogReader,type EnrollmentWorkflowOptions} from '@choiceapps/communityservices-sdk/browser';
3
+
4
+ /** Standalone encoders can discover exact labels before any option has been answered. */
5
+ export async function discoverLabels(api:WellSkyClient,enrollmentSession:EnrollmentCatalogReader,input:{clientId:number;assessmentCode:string}){
6
+ const definition=await api.assessments.getDefinition(input);
7
+ const optionIds=[...new Set(flattenQuestions(definition.questions).filter(({question})=>question.questionType==='lookup').flatMap(({question})=>question.options.map(o=>o.id)))];
8
+ const catalog=await enrollmentSession.readPicklistOptions({clientId:input.clientId,optionIds});
9
+ return {definition,writeLabel:createCatalogWriteLabelResolver(definition,catalog)};
10
+ }
11
+
12
+ /** Built-in enrollment sessions discover labels automatically when writeLabel is omitted. */
13
+ export function automaticWorkflow(options:Omit<EnrollmentWorkflowOptions,'writeLabel'|'yesNoWrites'|'enrollmentSession'>&{enrollmentSession:EnrollmentCatalogReader}){
14
+ return createEnrollmentWorkflow({...options,yesNoWrites:'standard'});
15
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@choiceapps/communityservices-sdk",
3
- "version": "0.7.0",
3
+ "version": "0.8.1",
4
4
  "description": "SDK for WellSky Community Services (HMIS), formerly ServicePoint, Client API V2 and V3",
5
5
  "type": "module",
6
6
  "main": "./dist/cjs/index.js",