@choiceapps/communityservices-sdk 0.8.0 → 0.9.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.
package/API.md CHANGED
@@ -84,3 +84,253 @@ 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_ENROLLMENT_GROUP_MISMATCH` | adoption enrollment group changed<br>adoption enrollment group mismatch |
111
+ | `ADOPTION_ENROLLMENT_HOUSEHOLD_MISMATCH` | adoption enrollment household mismatch<br>adoption household differs from enrollment association |
112
+ | `ADOPTION_EXIT_PRECEDES_ENTRY` | adoption exit precedes entry |
113
+ | `ADOPTION_HEAD_EVIDENCE_REQUIRED` | adoption head evidence required<br>native adoption requires original entry head relationship evidence |
114
+ | `ADOPTION_HOUSEHOLD_HEAD_MISMATCH` | adoption household head mismatch |
115
+ | `ADOPTION_HOUSEHOLD_MEMBERSHIP_MISMATCH` | adoption household membership mismatch |
116
+ | `ADOPTION_HOUSEHOLD_REQUIRED` | linked household enrollment requires adoptHouseholdEnrollment |
117
+ | `ADOPTION_KEY_CONFLICTS_WITH_PREVIOUS_INTENT` | adoption key conflicts with previous intent |
118
+ | `ADOPTION_REQUIRES_THE_ORIGINAL_ENROLLMENT_HEAD_RELATIONSHIP` | adoption requires the original enrollment head relationship |
119
+ | `AMBIGUOUS_ASSESSMENT_READ` | ambiguous assessment read |
120
+ | `AMBIGUOUS_CLOSURE_SNAPSHOT` | ambiguous closure snapshot |
121
+ | `AMBIGUOUS_CORRECTION_ASSESSMENT_READ` | ambiguous correction assessment read |
122
+ | `AMBIGUOUS_CORRECTION_ROW` | ambiguous correction row |
123
+ | `AMBIGUOUS_CORRECTION_ROW_CELL` | ambiguous correction row cell |
124
+ | `AMBIGUOUS_HOUSEHOLD_MEMBERSHIP` | ambiguous household membership |
125
+ | `ANOTHER_UNFINISHED_HOUSEHOLD_WORKFLOW_MUST_BE_RECONCILED_FIRST` | another unfinished household workflow must be reconciled first |
126
+ | `ANOTHER_UNFINISHED_WORKFLOW_MUST_BE_RECONCILED_FIRST` | another unfinished workflow must be reconciled first |
127
+ | `ANSWER_ENCODING_UNAVAILABLE` | answer encoding unavailable |
128
+ | `ASSESSMENT_FIELD_UNCOVERED` | assessment field uncovered |
129
+ | `ASSESSMENT_LOCATIONS_MUST_BE_A_MAP_BY_ENTRY_EXIT_TYPE` | assessment locations must be a map by Entry/Exit type |
130
+ | `ASSESSMENT_LOCATIONS_REQUIRED` | assessment locations required |
131
+ | `ASSESSMENT_METADATA_INCOMPLETE` | assessment metadata incomplete |
132
+ | `ASSESSMENT_PLAN_REQUIRED` | assessment plan required |
133
+ | `ATTEMPTED_CORRECTION_REQUIRED_FOR_RESOLUTION` | attempted correction required for resolution |
134
+ | `AUTHORITATIVE_ADOPTION_EXIT_DATE_REQUIRED` | authoritative adoption exit date required |
135
+ | `AUTHORITATIVE_ENROLLMENT_SESSION_REQUIRED_FOR_ADOPTION` | authoritative enrollment session required for adoption |
136
+ | `AUTHORITATIVE_EXIT_DETAILS_READER_REQUIRED` | authoritative exit details reader required |
137
+ | `AUTHORITATIVE_EXIT_READ_REQUIRED` | authoritative exit read required |
138
+ | `AUTHORITATIVE_HEAD_OF_HOUSEHOLD_EXIT_DATES_REQUIRED` | authoritative head of household exit dates required |
139
+ | `AUTHORITATIVE_INDIVIDUAL_EXIT_READ_REQUIRED` | authoritative individual exit read required |
140
+ | `CARRY_FORWARD_BIRTH_DATE_CHANGED_SINCE_ENROLLMENT` | carry-forward birth date changed since enrollment |
141
+ | `CARRY_FORWARD_BIRTH_DATE_IS_EMPTY_OR_FOLLOWS_ASSESSMENT_DATE` | carry-forward birth date is empty or follows assessment date |
142
+ | `CARRY_FORWARD_FIELD_IS_EMPTY` | carry-forward field is empty |
143
+ | `CLIENT_ID_REQUIRED` | client ID required |
144
+ | `CLIENT_PROVIDER_AND_SUPPORTED_ENTRY_EXIT_TYPE_REQUIRED` | client, provider and supported Entry/Exit type required |
145
+ | `CONFLICTING_FIELD_COVERAGE` | conflicting field coverage |
146
+ | `CORRECTION_ANSWER_COULD_NOT_BE_ENCODED` | correction answer could not be encoded |
147
+ | `CORRECTION_KEY_CONFLICTS_WITH_PREVIOUS_INTENT` | correction key conflicts with previous intent |
148
+ | `CORRECTION_LOOKUP_COULD_NOT_BE_RESOLVED` | correction lookup could not be resolved |
149
+ | `CORRECTION_REQUIRES_AN_EXISTING_ENTRY_ROW_KEY_AND_MATCHING_PARENT` | correction requires an existing entry row key and matching parent |
150
+ | `CORRECTION_ROW_METADATA_INCOMPLETE` | correction row metadata incomplete |
151
+ | `CORRECTION_SCALAR_READ_MISSING` | correction scalar read missing |
152
+ | `CORRECTION_STATE_CHANGED_DURING_RESOLUTION` | correction state changed during resolution |
153
+ | `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 |
154
+ | `CORRECTION_SUBASSESSMENT_READ_MISSING` | correction subassessment read missing |
155
+ | `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 |
156
+ | `DESTINATION_AND_REASON_FOR_LEAVING_REQUIRED` | destination and reason for leaving required |
157
+ | `DOB_AND_HOUSEHOLD_HEAD_CORRECTIONS_REQUIRE_THEIR_DEDICATED_WORKFLOWS` | DOB and household head corrections require their dedicated workflows |
158
+ | `DOB_HELPER_REQUIRED_FOR_BIRTH_DATE_ANSWERS_AND_CARRY_FORWARD` | DOB helper required for birth-date answers and carry-forward |
159
+ | `DOB_QUESTION_TYPE_CHANGED` | DOB question type changed |
160
+ | `DUPLICATE_ADOPTION_TARGETS` | duplicate adoption targets |
161
+ | `DUPLICATE_CORRECTION_JOURNAL_KEYS` | duplicate correction journal keys |
162
+ | `DUPLICATE_HOUSEHOLD_JOURNAL_KEYS` | duplicate household journal keys |
163
+ | `DUPLICATE_HOUSEHOLD_MEMBER_ENROLLMENT` | duplicate household member enrollment |
164
+ | `DUPLICATE_HOUSEHOLD_REVIEW_IDS` | duplicate household review IDs |
165
+ | `DUPLICATE_JOURNAL_KEYS` | duplicate journal keys |
166
+ | `DUPLICATE_MATCHING_ENROLLMENTS` | duplicate matching enrollments |
167
+ | `DUPLICATE_MATCHING_SUBASSESSMENT_ROWS` | duplicate matching subassessment rows |
168
+ | `DUPLICATE_SEMANTIC_SUBASSESSMENT_ROWS` | duplicate semantic subassessment rows |
169
+ | `DUPLICATE_SUBASSESSMENT_KEY` | duplicate subassessment key |
170
+ | `EMPTY_ENTRY_CORRECTION` | empty entry correction |
171
+ | `EMPTY_ROW_CORRECTION` | empty row correction |
172
+ | `END_DATE_PRECEDES_RECORD_START` | end date precedes record start |
173
+ | `END_DATE_TARGET_INTERVAL_MISSING` | end-date target interval missing |
174
+ | `END_DATE_TARGET_MISSING` | end-date target missing |
175
+ | `ENROLLMENT_ALREADY_JOURNALED_USE_ITS_EXISTING_ENTRY_KEY` | enrollment already journaled; use its existing entry key |
176
+ | `ENROLLMENT_COUNT_UNAVAILABLE_OR_CHANGED` | enrollment count unavailable or changed |
177
+ | `ENROLLMENT_LIST_CONTEXT_MISMATCH` | enrollment list context mismatch |
178
+ | `ENROLLMENT_LIST_EXCEEDS_LIMIT` | enrollment list exceeds limit |
179
+ | `ENROLLMENT_NOT_GROUPED` | standalone enrollment uses adoptEnrollment |
180
+ | `ENROLLMENT_READ_FAILED` | enrollment read failed |
181
+ | `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 |
182
+ | `ENTRY_CHANGED_SINCE_CORRECTION_PREFLIGHT` | entry changed since correction preflight |
183
+ | `ENTRY_CORRECTION_DID_NOT_VERIFY` | entry correction did not verify |
184
+ | `ENTRY_CORRECTION_ENROLLMENT_CONTEXT_MISMATCH` | entry correction enrollment context mismatch |
185
+ | `ENTRY_CORRECTION_MUST_PRECEDE_INTERIM_OR_EXIT_WORKFLOWS` | entry correction must precede interim or exit workflows |
186
+ | `ENTRY_CORRECTION_REJECTED` | correction was resolved as not applied; use a new key for a corrected intent<br>entry correction rejected |
187
+ | `ENTRY_CORRECTION_REQUIRES_AN_OPEN_ENROLLMENT_WITHOUT_REVIEWS` | entry correction requires an open enrollment without reviews |
188
+ | `ENTRY_CORRECTION_UNCERTAIN` | entry correction uncertain<br>uncertain entry correction requires reconciliation |
189
+ | `EXISTING_EXIT_CONFLICTS_WITH_REQUESTED_EXIT` | existing exit conflicts with requested exit |
190
+ | `EXISTING_HOUSEHOLD_REVIEW_CONFLICT` | existing household review conflict |
191
+ | `EXIT_ASSESSMENT_CONFIGURATION_CHANGED` | exit assessment configuration changed |
192
+ | `EXIT_IDENTITY_CHANGED` | exit identity changed |
193
+ | `EXIT_IDENTITY_MISSING` | exit identity missing |
194
+ | `EXIT_PRECEDES_AN_EXISTING_REVIEW` | exit precedes an existing review |
195
+ | `EXIT_SUBGROUP_IDENTITY_CHANGED` | exit subgroup identity changed |
196
+ | `EXIT_SUBGROUP_MEMBERSHIP_MISMATCH` | exit subgroup membership mismatch |
197
+ | `EXIT_UI_AND_AUTHORITATIVE_ENROLLMENT_SESSION_REQUIRED` | exit UI and authoritative enrollment session required |
198
+ | `EXIT_UI_DESTINATION_AND_REASON_FOR_LEAVING_REQUIRED` | exit UI, destination and reason for leaving required |
199
+ | `EXPLICIT_ENROLLMENT_TARGETS_REQUIRED` | explicit enrollment targets required |
200
+ | `HEAD_AND_HOUSEHOLD_MEMBERS_REQUIRED` | head and household members required |
201
+ | `HEAD_CLIENT_ID_REQUIRED` | head client ID required |
202
+ | `HEAD_OF_HOUSEHOLD_ASSESSMENT_OPTION_UNRESOLVED` | head of household assessment option unresolved |
203
+ | `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 |
204
+ | `HOUSEHOLD_API_AND_AUTHORITATIVE_ENROLLMENT_SESSION_REQUIRED` | household API and authoritative enrollment session required |
205
+ | `HOUSEHOLD_DISCOVERY_AMBIGUOUS` | a unique current household matching the full cohort, head and dates is required |
206
+ | `HOUSEHOLD_DISCOVERY_COHORT_CHANGED` | household membership changed during discovery<br>linked enrollment cohort changed during discovery<br>native household or enrollment cohort changed during discovery<br>native household or enrollment group changed during discovery |
207
+ | `HOUSEHOLD_DISCOVERY_DETAIL_INVALID` | complete linked enrollment cohort required<br>group flag conflicts with linked cohort |
208
+ | `HOUSEHOLD_DISCOVERY_INPUT_INVALID` | head client and enrollment IDs required |
209
+ | `HOUSEHOLD_DISCOVERY_LIST_INCOMPLETE` | household list count unavailable or changed<br>incomplete household list<br>invalid household list |
210
+ | `HOUSEHOLD_DISCOVERY_NATIVE_LINK_REQUIRED` | native household and enrollment group IDs required |
211
+ | `HOUSEHOLD_DISCOVERY_NOT_FOUND` | a unique current household matching the full cohort, head and dates is required |
212
+ | `HOUSEHOLD_DISCOVERY_READERS_REQUIRED` | authoritative enrollment reader required<br>legacy readers require complete household readers |
213
+ | `HOUSEHOLD_ENROLLMENT_CONTEXT_MISMATCH` | household enrollment context mismatch |
214
+ | `HOUSEHOLD_ENROLLMENT_IDENTITY_CHANGED` | household enrollment identity changed |
215
+ | `HOUSEHOLD_ENROLLMENT_MEMBERS_CHANGED` | household enrollment members changed |
216
+ | `HOUSEHOLD_EXITS_ARE_NOT_LINKED` | household exits are not linked |
217
+ | `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 |
218
+ | `HOUSEHOLD_KEY_CONFLICTS_WITH_PREVIOUS_INTENT` | household key conflicts with previous intent |
219
+ | `HOUSEHOLD_MEMBERS_UNAVAILABLE` | household members unavailable |
220
+ | `HOUSEHOLD_MEMBERSHIP_CHANGED_WHILE_ACQUIRING_LOCKS_RETRY` | household membership changed while acquiring locks; retry |
221
+ | `HOUSEHOLD_NATIVE_LINK_CHANGED` | household native link changed<br>native enrollment household or group changed |
222
+ | `HOUSEHOLD_ORIGINAL_HEAD_AND_COMPLETE_ENROLLMENT_COHORT_REQUIRED` | household, original head and complete enrollment cohort required |
223
+ | `HOUSEHOLD_PROGRAM_AND_SUPPORTED_ENTRY_EXIT_TYPE_REQUIRED` | household, program and supported Entry/Exit type required |
224
+ | `HOUSEHOLD_REVIEW_IDENTITY_CHANGED` | household review identity changed |
225
+ | `HOUSEHOLD_REVIEW_MEMBERSHIP_MISMATCH` | household review membership mismatch |
226
+ | `HOUSEHOLD_REVIEWS_UNAVAILABLE` | household reviews unavailable |
227
+ | `INCOMPLETE_ENROLLMENT_LIST` | incomplete enrollment list |
228
+ | `INCOMPLETE_EXISTING_EXIT_METADATA` | incomplete existing exit metadata |
229
+ | `INDEPENDENT_CONFIRMATION_REQUIRED` | independent non-application confirmation required |
230
+ | `INDIVIDUAL_ENROLLMENT_CONTEXT_MISMATCH` | individual enrollment context mismatch |
231
+ | `INDIVIDUAL_EXIT_MEMBERSHIP_MISMATCH` | individual exit membership mismatch |
232
+ | `INTERIM_DID_NOT_VERIFY` | interim did not verify |
233
+ | `INVALID_ASSESSMENT_PLAN` | invalid assessment plan |
234
+ | `INVALID_CORRECTION_CLIENT_OR_ROWS` | invalid correction client or rows |
235
+ | `INVALID_CORRECTION_DATE` | invalid correction date |
236
+ | `INVALID_CORRECTION_EFFECTIVE_DATE` | invalid correction effective date |
237
+ | `INVALID_ENROLLMENT_DATA_ADOPTED_MEMBERS_REQUIRE_MATCHING_PROGRAM_AND_HEAD_COVERAGE` | invalid enrollment data: adopted members require matching program and head coverage |
238
+ | `INVALID_ENROLLMENT_DATA_ENROLLMENT_HAS_NO_HEAD_OF_HOUSEHOLD` | invalid enrollment data: enrollment has no head of household |
239
+ | `INVALID_ENROLLMENT_DATA_HEAD_OF_HOUSEHOLD_RELATIONSHIP_IS_MISSING_OR_CLEARED` | invalid enrollment data: head of household relationship is missing or cleared |
240
+ | `INVALID_ENROLLMENT_DATA_HOUSEHOLD_HAS_NO_ACTIVE_HEAD_OF_HOUSEHOLD` | invalid enrollment data: household has no active head of household |
241
+ | `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 |
242
+ | `INVALID_ENROLLMENT_LIST` | invalid enrollment list |
243
+ | `INVALID_HOUSEHOLD_ENROLLMENT_MEMBERS` | invalid household enrollment members |
244
+ | `INVALID_HOUSEHOLD_JOURNAL` | invalid household journal |
245
+ | `INVALID_OR_CONFLICTING_CORRECTION_ANSWERS_AND_CLEARS` | invalid or conflicting correction answers and clears |
246
+ | `INVALID_OR_DUPLICATE_END_DATE_TARGET` | invalid or duplicate end-date target |
247
+ | `INVALID_OR_REUSED_SUBASSESSMENT_ID` | invalid or reused subassessment ID |
248
+ | `INVALID_STORED_CORRECTION_DATE` | invalid stored correction date |
249
+ | `INVALID_STORED_CORRECTION_NUMBER` | invalid stored correction number |
250
+ | `INVALID_SUBASSESSMENT_PLAN` | invalid subassessment plan |
251
+ | `INVALID_WORKFLOW_JOURNAL` | invalid workflow journal |
252
+ | `KEY_CONFLICTS_WITH_PREVIOUS_INTENT` | key conflicts with previous intent |
253
+ | `LINKED_HOUSEHOLD_ENROLLMENT_REQUIRES_EXITHOUSEHOLD` | linked household enrollment requires exitHousehold |
254
+ | `LOOKUP_WRITE_LABEL_UNAVAILABLE` | lookup write label unavailable |
255
+ | `MANDATORY_CLEAR_CONFLICT` | mandatory clear conflict |
256
+ | `MANDATORY_CORRECTION_FIELD_COULD_NOT_BE_ENCODED` | mandatory correction field could not be encoded |
257
+ | `MANDATORY_CORRECTION_FIELD_REQUIRES_AN_EXPLICIT_ANSWER_OR_CLEAR` | mandatory correction field requires an explicit answer or clear |
258
+ | `MANDATORY_CORRECTION_LOOKUP_LABEL_UNAVAILABLE` | mandatory correction lookup label unavailable |
259
+ | `MATCHING_CORRECTION_JOURNAL_REQUIRED` | matching correction journal required |
260
+ | `MEMBER_ALREADY_HAS_AN_INDEPENDENT_MATCHING_ENROLLMENT` | member already has an independent matching enrollment |
261
+ | `MEMBER_CLIENT_AND_ASSESSMENT_REQUIRED` | member client and assessment required |
262
+ | `MEMBER_ENROLLMENT_HAS_NOT_BEEN_ATTACHED` | member enrollment has not been attached |
263
+ | `MEMBER_ENTRY_DATE_PORT_REQUIRED` | member entry date port required |
264
+ | `MEMBER_EXIT_IDENTITY_MISSING` | member exit identity missing |
265
+ | `MEMBER_IS_NOT_ACTIVE_IN_HOUSEHOLD_AT_ENTRY` | member is not active in household at entry |
266
+ | `MOBILE_ANSWER_EXISTS` | mobile answer exists<br>mobile scalar answer already exists at the effective date |
267
+ | `MOBILE_PREFLIGHT_UNAVAILABLE` | mobile preflight unavailable<br>mobile scalar preflight unavailable |
268
+ | `MULTIPLE_END_DATE_OVERRIDES` | multiple end-date overrides |
269
+ | `NEW_MEMBER_MUST_REMAIN_ENROLLED` | new member must remain enrolled |
270
+ | `NOT_APPLICABLE_EXPLANATION_REQUIRED` | not-applicable explanation required |
271
+ | `OPEN_HOUSEHOLD_ENROLLMENT_REQUIRED_TO_ADD_A_MEMBER` | open household enrollment required to add a member |
272
+ | `PARTIAL_EXISTING_HOUSEHOLD_EXIT_REQUIRES_RECONCILIATION` | partial existing household exit requires reconciliation |
273
+ | `PARTIAL_HOUSEHOLD_REVIEW_REQUIRES_RECONCILIATION` | partial household review requires reconciliation |
274
+ | `PICKLIST_CATALOG_CONTEXT_MISMATCH` | picklist catalog client mismatch<br>picklist catalog context mismatch |
275
+ | `PROGRAM_AND_SUPPORTED_ENTRY_EXIT_TYPE_REQUIRED` | program and supported Entry/Exit type required |
276
+ | `PROGRAM_ASSESSMENT_CONFIGURATION_CHANGED` | program assessment configuration changed |
277
+ | `PROGRAM_ASSESSMENT_DEFAULT_UNRESOLVED` | program assessment default unresolved |
278
+ | `PROVIDER_DISCOVERY_MISMATCH` | provider discovery mismatch |
279
+ | `RECORD_ALREADY_HAS_A_DIFFERENT_END_DATE` | record already has a different end date |
280
+ | `RESOLUTION_EVIDENCE_REFERENCE_CHANGED` | resolution evidence reference changed |
281
+ | `REST_API_DISCOVERY_ENROLLMENT_HELPERS_AND_DURABLE_STORE_REQUIRED` | REST API, discovery, enrollment helpers and durable store required |
282
+ | `REVIEW_FOLLOWS_PROJECT_EXIT` | review follows project exit |
283
+ | `REVIEW_PRECEDES_ENTRY` | review precedes entry |
284
+ | `ROW_CLOSURE_MUST_FALL_BETWEEN_ENTRY_AND_ASSESSMENT` | row closure must fall between entry and assessment |
285
+ | `ROW_REGISTRATION_BINDING_CONFLICT` | row key or recordset is already bound differently |
286
+ | `ROW_REGISTRATION_CONTEXT_MISMATCH` | enrollment identity changed during row registration |
287
+ | `ROW_REGISTRATION_ENTRY_REQUIRED` | verified enrollment workflow required |
288
+ | `ROW_REGISTRATION_INPUT_INVALID` | unique row keys, parents and positive recordset IDs required<br>valid registration key, entry key, client and explicit rows required |
289
+ | `ROW_REGISTRATION_INTENT_CONFLICT` | registration key conflicts with previous intent |
290
+ | `ROW_REGISTRATION_PARENT_INVALID` | subassessment parent not in entry definition |
291
+ | `ROW_REGISTRATION_READ_UNAVAILABLE` | complete row registration read required |
292
+ | `ROW_REGISTRATION_READER_REQUIRED` | authoritative enrollment reader required |
293
+ | `ROW_REGISTRATION_ROW_MISSING` | selected row is missing or ambiguous at the entry date or current read |
294
+ | `SAVED_ENROLLMENT_MISSING` | saved enrollment missing |
295
+ | `SAVED_ENROLLMENT_MISSING_OR_CHANGED` | saved enrollment missing or changed |
296
+ | `SESSION_INTERRUPTED` | session interrupted<br>upstream workflow operation failed |
297
+ | `STABLE_CORRECTION_KEY_REQUIRED` | stable correction key required |
298
+ | `STABLE_KEY_REQUIRED` | stable key required |
299
+ | `STANDARD_ASSESSMENT_WRITES_AND_AUTHORITATIVE_ENROLLMENT_READER_REQUIRED_FOR_CORRECTIONS` | standard assessment writes and authoritative enrollment reader required for corrections |
300
+ | `STORED_EXIT_DETAILS_CONFLICT_WITH_REQUESTED_EXIT` | stored exit details conflict with requested exit |
301
+ | `SUBASSESSMENT_READ_MISSING` | subassessment read missing |
302
+ | `TIMEZONE_QUALIFIED_WHOLE_SECOND_DATE_REQUIRED` | timezone-qualified whole-second date required |
303
+ | `UNCERTAIN_ENTRY_CORRECTION_REQUIRES_RECONCILIATION` | uncertain entry correction requires reconciliation |
304
+ | `UNCERTAIN_HOUSEHOLD_EXIT_REQUIRES_RECONCILIATION` | uncertain household exit requires reconciliation |
305
+ | `UNCERTAIN_MEMBER_ENTRY_DATE_REQUIRES_RECONCILIATION` | uncertain member entry date requires reconciliation |
306
+ | `UNCERTAIN_WRITE_REQUIRES_RECONCILIATION` | uncertain write requires reconciliation |
307
+ | `UNFINISHED_HOUSEHOLD_WORKFLOW_MUST_BE_RECONCILED_FIRST` | unfinished household workflow must be reconciled first |
308
+ | `UNIQUE_MEMBERS_INCLUDING_HEAD_REQUIRED` | unique members including head required |
309
+ | `UNKNOWN_CARRY_FORWARD_FIELD` | unknown carry-forward field |
310
+ | `UNKNOWN_OR_INVALID_CORRECTION_FIELDS` | unknown or invalid correction fields |
311
+ | `UNKNOWN_OR_INVALID_INPUT_FIELDS` | unknown or invalid input fields |
312
+ | `UNSELECTED_HOUSEHOLD_MEMBER_CHANGED` | unselected household member changed |
313
+ | `UNSELECTED_MEMBER_EXIT_IDENTITY_MISSING` | unselected member exit identity missing |
314
+ | `UNSUPPORTED_CORRECTION_FIELD_TYPE` | unsupported correction field type |
315
+ | `UNSUPPORTED_ENTRY_EXIT_TYPE_IN_ASSESSMENT_LOCATIONS` | unsupported Entry/Exit type in assessment locations |
316
+ | `UNSUPPORTED_MANDATORY_CORRECTION_FIELD` | unsupported mandatory correction field |
317
+ | `UNSUPPORTED_MANDATORY_FIELD_TYPE` | unsupported mandatory field type |
318
+ | `UNSUPPORTED_YES_NO_CLEAR` | unsupported yes no clear<br>yes/no answers cannot be cleared |
319
+ | `VERIFIED_CORRECTION_CANNOT_BE_REJECTED` | verified correction cannot be rejected |
320
+ | `VERIFIED_ENROLLMENT_WORKFLOW_REQUIRED` | verified enrollment workflow required |
321
+ | `VERIFIED_ENTRY_CORRECTION_CHANGED` | verified entry correction changed |
322
+ | `VERIFIED_EXIT_MISSING` | verified exit missing |
323
+ | `VERIFIED_HOUSEHOLD_ENTRY_REQUIRED` | verified household entry required |
324
+ | `VERIFIED_HOUSEHOLD_ENTRY_WITH_IDENTICAL_MEMBERS_REQUIRED_UNKNOWN_SELECTED_MEMBER` | verified household entry with identical members required; unknown selected member |
325
+ | `VERIFIED_ROW_CORRECTION_CHANGED` | verified row correction changed |
326
+ | `VERIFIED_STEP_CHANGED` | verified step changed |
327
+ | `WORKFLOW_CONFIGURATION_INVALID` | invalid workflow configuration<br>workflow configuration invalid |
328
+ | `WORKFLOW_FAILED` | Unclassified workflow failure; inspect cause and commit markers. |
329
+ | `WORKFLOW_JOURNAL_INVALID` | invalid workflow journal |
330
+ | `WORKFLOW_PENDING` | unfinished household workflow must be reconciled first<br>unfinished workflow must be reconciled first |
331
+ | `WORKFLOW_SCHEMA_CHANGED` | program assessment configuration changed |
332
+ | `WORKFLOW_UPSTREAM_FAILURE` | upstream workflow operation failed<br>workflow upstream failure |
333
+ | `WORKFLOW_VALIDATION_FAILED` | upstream workflow operation failed<br>workflow validation failed |
334
+ | `YES_NO_ANSWERS_CANNOT_BE_CLEARED` | yes/no answers cannot be cleared |
335
+ | `YES_NO_WRITE_MODE_CHANGED_SINCE_THIS_WORKFLOW_STARTED` | yes/no write mode changed since this workflow started |
336
+ | `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,25 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.9.1 - 2026-11-11
4
+
5
+ - Keep the pinned Chromium download and full browser checks on GitHub Ubuntu 24.04 runners while using their preinstalled runtime libraries. Avoid the redundant APT/font installation that blocked 0.9.0 public publication when the Ubuntu mirror failed DNS resolution. SDK runtime behavior is unchanged from 0.9.0.
6
+
7
+ ## 0.9.0 - 2026-11-11
8
+
9
+ - Expose native `householdId` on managed GWT enrollment list/detail reads, separately from `groupId`.
10
+ - Discover and adopt linked enrollments from the head's IDs using the recorded household association, with cohort-wide consistency and original-head checks. Explicit household IDs must match the enrollment record.
11
+ - Use pinned native household/group identity for existing-group interims and exits, allowing changed current membership while preserving original enrollment-head coverage. Creation and additions still validate current membership.
12
+ - Preserve existing adoption keys and legacy custom-reader behavior; discovery reports whether evidence is `native-enrollment` or `verified-membership`.
13
+ - Verify native reads on existing training Coordinated Entry groups and read-only adoption of open/exited groups with no-write retries. No live enrollment or assessment records were changed.
14
+
15
+ ## 0.8.1 - 2026-11-11
16
+
17
+ - 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.
18
+ - Give complete-cohort adoption, standalone adoption of grouped entries, and changed verified steps specific stable codes instead of `WORKFLOW_FAILED`.
19
+ - Generate the complete enrollment workflow error-code reference in `API.md`; fail generation for new constant refusal reasons without codes, including conditional branches.
20
+ - Clarify that household discovery verifies current membership, not the original historical association, and explicit IDs do not bypass active-membership validation.
21
+ - 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.
22
+
3
23
  ## 0.8.0 - 2026-11-11
4
24
 
5
25
  - Resolve full lookup write labels without existing answers using `enrollmentSession.readPicklistOptions` and `createCatalogWriteLabelResolver`.
@@ -110,10 +110,9 @@ export interface AdoptHouseholdEnrollmentInput {
110
110
  members:Array<{clientId:number;enrollmentId:number}>;
111
111
  }
112
112
  export interface HouseholdEnrollmentDiscoveryInput {headClientId:number;headEnrollmentId:number;}
113
- export interface HouseholdEnrollmentDiscoveryResult extends HouseholdEnrollmentDiscoveryInput {
114
- householdId:number;householdSource:'verified-membership';providerId:number;type:string;
115
- members:Array<{clientId:number;enrollmentId:number}>;
116
- }
113
+ export type HouseholdEnrollmentDiscoveryResult = HouseholdEnrollmentDiscoveryInput & {
114
+ householdId:number;providerId:number;type:string;members:Array<{clientId:number;enrollmentId:number}>;
115
+ } & ({householdSource:'native-enrollment';groupId:number|string}|{householdSource:'verified-membership';groupId?:never});
117
116
  export interface AdoptHouseholdEnrollmentFromHeadInput extends HouseholdEnrollmentDiscoveryInput {key:string;}
118
117
  export interface RegisterEntryRowsInput {
119
118
  key:string;enrollmentKey:string;clientId:number;
@@ -220,6 +219,8 @@ export interface HouseholdExitInput extends HouseholdExitDetailInput {
220
219
  members:HouseholdMemberPlan[];
221
220
  }
222
221
  export interface HouseholdEnrollmentResult {
222
+ groupId?:number|string;
223
+ householdSource?:'native-enrollment';
223
224
  key:string;
224
225
  outcome:'verified'|'already-present';
225
226
  householdId:number;
@@ -229,6 +230,8 @@ export interface HouseholdEnrollmentResult {
229
230
  export interface HouseholdInterimResult extends Omit<HouseholdEnrollmentResult,'members'> {members:InterimWorkflowResult[];}
230
231
  export interface HouseholdExitResult extends Omit<HouseholdEnrollmentResult,'members'> {members:ExitWorkflowResult[];}
231
232
  export interface HouseholdWorkflowStatus {
233
+ groupId?:number|string;
234
+ householdSource?:'native-enrollment';
232
235
  key:string;
233
236
  kind:'entry'|'addition'|'interim'|'exit';
234
237
  stage:'pending'|'verified';
@@ -246,7 +249,7 @@ export interface EnrollmentWorkflowOptions {
246
249
  store:UiMutationStore;
247
250
  /** Required for SVPPROFDOB answers or carry-forward. Share this workflow's durable store. */
248
251
  birthDates?:DateOfBirth;
249
- /** Required for exits; also required with api.households and helpers.addHouseholdMembers for household operations. */
252
+ /** Required for exits and household operations. Household creation/additions also need api.households and helpers.addHouseholdMembers; native-linked adoption/reviews/exits do not. */
250
253
  enrollmentSession?:EnrollmentReader;
251
254
  /** Required for exitEnrollment and exitHousehold; the workflow owns its durable write journal. */
252
255
  exitUi?:HouseholdExitUi;
@@ -10,6 +10,7 @@ import {readProgramCatalog,flattenQuestions,createCatalogWriteLabelResolver,enco
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;
13
+ const validGroupId=v=>positive(v)||typeof v==='string'&&/^[a-zA-Z0-9_-]+$/.test(v);
13
14
  const entryExitTypes=['HUD','Basic','Standard','RHY','VA','Basic Center Program Entry/Exit','PATH','Quick Call','Transitional Living Program Entry/Exit'];
14
15
  const canonical=v=>JSON.stringify(v,(_,x)=>object(x)?Object.fromEntries(Object.keys(x).sort().map(k=>[k,x[k]])):x);
15
16
  const hash=v=>createHash('sha256').update(canonical(v)).digest('hex');
@@ -251,20 +252,20 @@ export function createEnrollmentWorkflow({api,discovery,helpers,store,enrollment
251
252
  // assessment's own rows. Read at the later of now and the assessment instant.
252
253
  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'}}));
253
254
  // A generic read at exactly the effective instant returns null for yes/no answers saved at
254
- // that instant; one second later returns them. Only the yes/no step needs this.
255
+ // that instant; one second later returns them. Scalar preflight and verification use this.
255
256
  const readSaved=async()=>questions(await api.assessments.get({path:{clientId:input.clientId,assessmentCode:code},query:{dateEffective:new Date(Date.parse(at)+1000).toISOString()}}));
256
- const initial=await read();
257
- if(!record&&plan.scalar.length){
258
- const saved=await readSaved();
257
+ const assertScalarInsertable=saved=>{
259
258
  const existing=plan.scalar.map(q=>({q,row:saved.find(r=>r.virt_field_name===q.virt_field_name)}));
260
- const complete=existing.every(({q,row})=>row&&sameAnswer(q,row.answer_value)&&restDate(row.date_effective)===restDate(at));
259
+ const complete=existing.every(({q,row})=>row&&row.question_type===q.question_type&&sameAnswer(q,row.answer_value)&&restDate(row.date_effective)===restDate(at));
261
260
  if(!complete){
262
261
  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);
263
- if(unavailable.length)fail('mobile scalar preflight unavailable',{code:'MOBILE_PREFLIGHT_UNAVAILABLE',fields:unavailable});
262
+ if(unavailable.length)fail('mobile scalar preflight unavailable',{code:'MOBILE_PREFLIGHT_UNAVAILABLE',step:'scalars',fields:unavailable});
264
263
  const collisions=existing.filter(({row})=>restDate(row.date_effective)===restDate(at)).map(({q})=>q.virt_field_name);
265
- if(collisions.length)fail('mobile scalar answer already exists at the effective date',{code:'MOBILE_ANSWER_EXISTS',fields:collisions});
264
+ if(collisions.length)fail('mobile scalar answer already exists at the effective date',{code:'MOBILE_ANSWER_EXISTS',step:'scalars',fields:collisions});
266
265
  }
267
- }
266
+ };
267
+ const initial=await read();
268
+ if(!record&&plan.scalar.length)assertScalarInsertable(await readSaved());
268
269
  if(expectedHead===input.clientId&&headQuestion)checkHeadAnswer(proposedHead?.answer_value??initial.find(q=>q.virt_field_name==='HUD_RELATIONTOHOH')?.answer_value);
269
270
  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');}
270
271
  // Resend mandatory answers with the standard yes/no write. A blank write must not hide a recorded value.
@@ -370,7 +371,13 @@ export function createEnrollmentWorkflow({api,discovery,helpers,store,enrollment
370
371
  },()=>api.subassessments.setEndDate({clientId:input.clientId,assessmentCode:code,effectiveDate:at,parentField:end.parentField,recordsetId:end.recordsetId,endDateField:end.endDateField,endDate:end.endDate}));
371
372
  }
372
373
  if(plan.scalar.length)await step('scalars',async()=>{
373
- 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)};
374
+ const source=await readSaved();
375
+ 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));});
376
+ // Enrollment/review creation or another writer can populate shared questions after
377
+ // initial preflight. Refuse a known collision before journaling this first attempt.
378
+ // Prior attempts remain reconciliation-only, even when only part of a batch saved.
379
+ if(!found&&!record.steps.scalars)assertScalarInsertable(source);
380
+ return {found,value:plan.scalar.map(q=>q.virt_field_name)};
374
381
  },()=>api.assessmentsMobile.addAnswer({path:{clientId:input.clientId,assessmentCode:code},query:{dateEffective:at},body:plan.scalar}));
375
382
  // Standard writes update existing dated rows and change only the answers sent.
376
383
  if(plan.yesNo.length){const body=[...plan.yesNo,...mandatory.map(({expected,...q})=>q)],expected=[...plan.yesNo,...mandatory.map(({expected,...q})=>({...q,answer_value:expected}))];
@@ -427,7 +434,7 @@ export function createEnrollmentWorkflow({api,discovery,helpers,store,enrollment
427
434
  for(const target of targets){fields(target,['clientId','enrollmentId']);if(!positive(target.clientId)||!positive(target.enrollmentId))fail('explicit enrollment targets required');}
428
435
  targets.sort((a,b)=>a.clientId-b.clientId);const ids=targets.map(t=>t.clientId);
429
436
  if(new Set(ids).size!==ids.length||new Set(targets.map(t=>t.enrollmentId)).size!==targets.length)fail('duplicate adoption targets');
430
- if(household&&(!positive(input.householdId)||!positive(input.headClientId)||!ids.includes(input.headClientId)||typeof api.households?.get!=='function'))fail('household, original head and complete enrollment cohort required');
437
+ if(household&&(!positive(input.householdId)||!positive(input.headClientId)||!ids.includes(input.headClientId)))fail('household, original head and complete enrollment cohort required');
431
438
  const owner=hash({adoption:true,input}),intentHash=hash(input);
432
439
  return withClients(ids,async()=>{
433
440
  const locked=household?work=>store.withLock(householdScope(input.headClientId),work):work=>work();
@@ -460,7 +467,13 @@ export function createEnrollmentWorkflow({api,discovery,helpers,store,enrollment
460
467
  }
461
468
  const head=snapshots.find(r=>r.target.clientId===(household?input.headClientId:input.clientId));
462
469
  if(snapshots.some(r=>r.s.providerId!==head.s.providerId||r.s.enrollmentType!==head.s.enrollmentType||r.start<head.start||r.end>head.end))fail('invalid enrollment data: adopted members require matching program and head coverage');
463
- if(household){
470
+ const native=household&&snapshots.some(r=>r.s.householdId!==undefined);
471
+ if(native){
472
+ if(snapshots.some(r=>r.s.householdId!==input.householdId))fail('adoption household differs from enrollment association',{code:'ADOPTION_ENROLLMENT_HOUSEHOLD_MISMATCH'});
473
+ if(!validGroupId(head.s.groupId)||snapshots.some(r=>r.s.groupId!==head.s.groupId||r.s.grouped===false)||group?.context.groupId!==undefined&&group.context.groupId!==head.s.groupId)fail('adoption enrollment group changed',{code:'ADOPTION_ENROLLMENT_GROUP_MISMATCH'});
474
+ }
475
+ if(household&&!native){
476
+ if(typeof api.households?.get!=='function')fail('household, original head and complete enrollment cohort required');
464
477
  const found=toItems(await api.households.get({path:{householdId:input.householdId}}));
465
478
  if(found.length!==1||found[0].householdId!==input.householdId||found[0].isActive===false||!Array.isArray(found[0].clients))fail('active household required for adoption');
466
479
  const members=found[0].clients,heads=members.filter(m=>m.headofhouseholdStatus&&m.isActive!==false&&!m.dateLeft);
@@ -472,10 +485,11 @@ export function createEnrollmentWorkflow({api,discovery,helpers,store,enrollment
472
485
  const context={clientId:r.target.clientId,providerId:r.s.providerId,type:r.s.enrollmentType,entryDate:new Date(r.start).toISOString()};
473
486
  const discovered=await discover(context),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}});
474
487
  const headQuestion=discovered.entry.definition.questions.find(q=>q.fieldName==='HUD_RELATIONTOHOH');
488
+ if(native&&!headQuestion&&group?.stage!=='verified')fail('native adoption requires original entry head relationship evidence',{code:'ADOPTION_HEAD_EVIDENCE_REQUIRED'});
475
489
  if(headQuestion){
476
490
  const self=headQuestion.options.filter(o=>o.code==='RELATIONHOH_SELF'),answers=questions(await api.assessments.get({path:{clientId:r.target.clientId,assessmentCode:discovered.entry.assessment.code},query:{dateEffective:context.entryDate}}));
477
491
  const answer=answers.find(q=>q.virt_field_name==='HUD_RELATIONTOHOH')?.answer_value;
478
- if(self.length!==1||answer===null||answer===undefined||answer===''||(String(answer)===String(self[0].id))!==(r.target.clientId===head.target.clientId))fail('adoption requires the original enrollment head relationship');
492
+ if(self.length!==1||answer===null||answer===undefined||answer===''||!headQuestion.options.some(o=>String(o.id)===String(answer))||(String(answer)===String(self[0].id))!==(r.target.clientId===head.target.clientId))fail('adoption requires the original enrollment head relationship');
479
493
  }
480
494
  const existing=prior.get(r.target.clientId);
481
495
  if(existing&&(existing.enrollmentId!==r.target.enrollmentId||canonical(existing.context)!==canonical(context)||existing.schemaHash!==schemaHash))fail('adopted enrollment identity or configuration changed');
@@ -485,17 +499,18 @@ export function createEnrollmentWorkflow({api,discovery,helpers,store,enrollment
485
499
  const results=prepared.map(r=>({key:r.key,outcome:wasVerified?'already-present':'verified',clientId:r.context.clientId,enrollmentId:r.enrollmentId,assessmentCode:r.assessmentCode,effectiveDate:r.context.entryDate,recordsetIds:structuredClone(r.rows)}));
486
500
  if(household){
487
501
  if(!group){group={version:1,key:input.key,kind:'entry',adopted:true,intentHash,context:{householdId:input.householdId,headClientId:input.headClientId,providerId:head.s.providerId,type:head.s.enrollmentType,entryDate:new Date(head.start).toISOString()},clientIds:ids,stage:'pending',members:{},attached:true};groups.push(group);}
502
+ if(native){group.context.groupId=head.s.groupId;group.context.householdSource='native-enrollment';}
488
503
  await store.write(householdScope(input.headClientId),structuredClone(groups));
489
504
  for(const clientId of ids)await store.withLock(reservationScope(clientId),()=>store.write(reservationScope(clientId),[{owner,stage:'pending'}]));
490
505
  }
491
506
  for(const r of prepared){if(!prior.get(r.context.clientId))journals.get(r.context.clientId).push(r);await store.write(scope(r.context.clientId),structuredClone(journals.get(r.context.clientId)));}
492
- if(household){group.members=Object.fromEntries(results.map(r=>[r.clientId,r]));group.stage='verified';await store.write(householdScope(input.headClientId),structuredClone(groups));for(const clientId of ids)await store.withLock(reservationScope(clientId),()=>store.write(reservationScope(clientId),[{owner,stage:'verified'}]));return {key:input.key,outcome:wasVerified?'already-present':'verified',householdId:input.householdId,headClientId:input.headClientId,members:results};}
507
+ if(household){group.members=Object.fromEntries(results.map(r=>[r.clientId,r]));group.stage='verified';await store.write(householdScope(input.headClientId),structuredClone(groups));for(const clientId of ids)await store.withLock(reservationScope(clientId),()=>store.write(reservationScope(clientId),[{owner,stage:'verified'}]));return {key:input.key,outcome:wasVerified?'already-present':'verified',householdId:input.householdId,...(native?{groupId:head.s.groupId,householdSource:'native-enrollment'}:{}),headClientId:input.headClientId,members:results};}
493
508
  return results[0];
494
509
  });
495
510
  });
496
511
  };
497
512
  const householdRun=async(kind,raw,operation={attempted:false})=>{
498
- if(typeof enrollmentSession?.readEnrollment!=='function'||typeof helpers.addHouseholdMembers!=='function'||typeof api.households?.get!=='function')fail('household API and authoritative enrollment session required');
513
+ if(typeof enrollmentSession?.readEnrollment!=='function'||['entry','addition'].includes(kind)&&(typeof helpers.addHouseholdMembers!=='function'||typeof api.households?.get!=='function'))fail('household API and authoritative enrollment session required');
499
514
  fields(raw,kind==='addition'?['key','enrollmentKey','headClientId','entryDate','members']:kind==='entry'?['key','householdId','headClientId','providerId','type','entryDate','members']:['key','enrollmentKey','headClientId',kind==='exit'?'exitDate':'reviewDate','members',...(kind==='exit'?['destination','reasonForLeaving','destinationDetails','reasonForLeavingOther']:[])]);
500
515
  const input=structuredClone(raw);key(input.key);
501
516
  if(kind==='exit'&&(typeof exitUi?.saveExit!=='function'||typeof exitUi?.validateExit!=='function'||typeof input.destination!=='string'||!input.destination.trim()||typeof input.reasonForLeaving!=='string'||!input.reasonForLeaving.trim()))fail('exit UI, destination and reason for leaving required');
@@ -537,14 +552,25 @@ export function createEnrollmentWorkflow({api,discovery,helpers,store,enrollment
537
552
  const cohortIds=[...new Set([...Object.keys(catalog).map(Number),...ids])].sort((a,b)=>a-b);
538
553
  const context=kind==='entry'?{householdId:input.householdId,headClientId:input.headClientId,providerId:input.providerId,type:input.type,entryDate:input.entryDate}:kind==='addition'?{...entry.context,entryDate:input.entryDate}:entry.context;
539
554
  if(!['entry','addition'].includes(kind)&&ids.some(id=>restDate(input.exitDate??input.reviewDate)<restDate(catalog[id].entryDate)))fail('review precedes entry');
540
- const household=toItems(await api.households.get({path:{householdId:context.householdId}}));
541
- if(household.length!==1||household[0].householdId!==context.householdId||household[0].isActive===false||!Array.isArray(household[0].clients))fail('active household required');
542
- const people=household[0].clients;
543
- if(new Set(people.map(p=>p.clientId)).size!==people.length)fail('ambiguous household membership');
544
- const heads=people.filter(p=>p.headofhouseholdStatus&&p.isActive!==false&&!p.dateLeft);
545
- if(heads.length===0)fail('invalid enrollment data: household has no active head of household');
546
- if(heads.length!==1||heads[0].clientId!==input.headClientId)fail('household head mismatch; head changes are a red flag and require separate retroactive correction or a fresh enrollment');
547
- for(const clientId of ids){const p=people.find(p=>p.clientId===clientId);if(!p||p.isActive===false||p.dateLeft||!Number.isFinite(restDate(p.dateJoined))||restDate(p.dateJoined)>restDate(catalog[clientId]?.entryDate??context.entryDate))fail('member is not active in household at entry');}
555
+ let nativeGroupId;
556
+ if(!['entry','addition'].includes(kind)){
557
+ const anchor=await enrollmentSession.readEnrollment({clientId:input.headClientId,enrollmentId:catalog[input.headClientId]?.enrollmentId});
558
+ if(anchor?.householdId!==undefined||entry.context.groupId!==undefined||record?.context.groupId!==undefined){
559
+ if(!positive(anchor?.householdId)||anchor.householdId!==context.householdId||!validGroupId(anchor?.groupId)||anchor.grouped===false||entry.context.groupId!==undefined&&entry.context.groupId!==anchor.groupId||record?.context.groupId!==undefined&&record.context.groupId!==anchor.groupId)fail('native enrollment household or group changed',{code:'HOUSEHOLD_NATIVE_LINK_CHANGED'});
560
+ nativeGroupId=anchor.groupId;
561
+ }
562
+ }
563
+ if(nativeGroupId===undefined){
564
+ if(typeof api.households?.get!=='function')fail('household API and authoritative enrollment session required');
565
+ const household=toItems(await api.households.get({path:{householdId:context.householdId}}));
566
+ if(household.length!==1||household[0].householdId!==context.householdId||household[0].isActive===false||!Array.isArray(household[0].clients))fail('active household required');
567
+ const people=household[0].clients;
568
+ if(new Set(people.map(p=>p.clientId)).size!==people.length)fail('ambiguous household membership');
569
+ const heads=people.filter(p=>p.headofhouseholdStatus&&p.isActive!==false&&!p.dateLeft);
570
+ if(heads.length===0)fail('invalid enrollment data: household has no active head of household');
571
+ if(heads.length!==1||heads[0].clientId!==input.headClientId)fail('household head mismatch; head changes are a red flag and require separate retroactive correction or a fresh enrollment');
572
+ for(const clientId of ids){const p=people.find(p=>p.clientId===clientId);if(!p||p.isActive===false||p.dateLeft||!Number.isFinite(restDate(p.dateJoined))||restDate(p.dateJoined)>restDate(catalog[clientId]?.entryDate??context.entryDate))fail('member is not active in household at entry');}
573
+ }
548
574
  for(const clientId of cohortIds){const reservations=await store.withLock(reservationScope(clientId),()=>store.read(reservationScope(clientId)));if(!Array.isArray(reservations)||reservations.some(r=>r.stage!=='verified'&&r.owner!==owner))fail('unfinished household workflow must be reconciled first');}
549
575
  const validateSnapshot=(snapshot,target,{partial=false}={})=>{
550
576
  if(snapshot?.source!=='gwt-rpc'||snapshot.tenant!==api.tenant||snapshot.clientId!==target.clientId||snapshot.enrollmentId!==target.enrollmentId||snapshot.providerId!==context.providerId||snapshot.enrollmentType!==context.type||restDate(snapshot.projectStartDate)!==restDate(catalog[target.clientId]?.entryDate??context.entryDate))fail('household enrollment context mismatch');
@@ -553,7 +579,9 @@ export function createEnrollmentWorkflow({api,discovery,helpers,store,enrollment
553
579
  if(!Array.isArray(members)||members.some(m=>!positive(m.clientId)||!positive(m.enrollmentId))||new Set(members.map(m=>m.clientId)).size!==members.length||new Set(members.map(m=>m.enrollmentId)).size!==members.length||!members.some(m=>m.clientId===target.clientId&&m.enrollmentId===target.enrollmentId))fail('invalid household enrollment members');
554
580
  const actual=members.map(m=>m.clientId).sort((a,b)=>a-b);
555
581
  if(partial?actual.some(id=>!cohortIds.includes(id)):canonical(actual)!==canonical(cohortIds))fail('household enrollment members changed');
556
- if(!Array.isArray(snapshot.householdClientIds)||cohortIds.some(id=>!snapshot.householdClientIds.includes(id)))fail('household members unavailable');
582
+ const pinnedGroup=nativeGroupId??context.groupId;
583
+ if(pinnedGroup!==undefined){if(snapshot.householdId!==context.householdId||snapshot.groupId!==pinnedGroup||snapshot.grouped===false)fail('native enrollment household or group changed',{code:'HOUSEHOLD_NATIVE_LINK_CHANGED'});}
584
+ if(nativeGroupId===undefined&&(!Array.isArray(snapshot.householdClientIds)||cohortIds.some(id=>!snapshot.householdClientIds.includes(id))))fail('household members unavailable');
557
585
  for(const m of members){const known=record?.members[m.clientId]?.enrollmentId??catalog[m.clientId]?.enrollmentId;if(known&&known!==m.enrollmentId)fail('household enrollment identity changed');}
558
586
  return snapshot;
559
587
  };
@@ -583,6 +611,7 @@ export function createEnrollmentWorkflow({api,discovery,helpers,store,enrollment
583
611
  const preflight=[];for(const member of input.members)preflight.push(await run(kind==='addition'?'entry':kind,toMember(member),{locked:true,owner,operation,headClientId:input.headClientId,preflight:true,...(kind==='addition'?{pinSchema:true}:{})}));
584
612
  if(kind==='addition'&&record?.entrySchemas&&canonical(record.entrySchemas)!==canonical(preflight))fail('additional member assessment configuration changed');
585
613
  if(kind==='exit'&&record?.exitSchemas&&canonical(record.exitSchemas)!==canonical(preflight))fail('exit assessment configuration changed');
614
+ if(nativeGroupId!==undefined){context.groupId=nativeGroupId;context.householdSource='native-enrollment';entry.context.groupId=nativeGroupId;entry.context.householdSource='native-enrollment';if(record){record.context.groupId=nativeGroupId;record.context.householdSource='native-enrollment';}}
586
615
  const wasVerified=record?.stage==='verified';
587
616
  if(!record){record={version:1,key:input.key,kind,intentHash,context,...(input.enrollmentKey?{enrollmentKey:input.enrollmentKey}:{}),clientIds:ids,stage:'pending',members:{},...(kind==='exit'?{exitSchemas:preflight}:kind==='addition'?{entrySchemas:preflight}:{})};records.push(record);}
588
617
  const persist=()=>store.write(householdScope(input.headClientId),structuredClone(records));await persist();
@@ -742,7 +771,7 @@ export function createEnrollmentWorkflow({api,discovery,helpers,store,enrollment
742
771
  if(kind==='exit'||kind==='addition')await preserve();
743
772
  await validateHeadCoverage();
744
773
  record.stage='verified';await persist();for(const clientId of cohortIds)await store.withLock(reservationScope(clientId),()=>store.write(reservationScope(clientId),[{owner,stage:'verified'}]));
745
- return {key:input.key,outcome:wasVerified?'already-present':'verified',householdId:context.householdId,headClientId:input.headClientId,members:ids.map(id=>structuredClone(record.members[id]))};
774
+ return {key:input.key,outcome:wasVerified?'already-present':'verified',householdId:context.householdId,...(context.groupId!==undefined?{groupId:context.groupId,householdSource:context.householdSource}:{}),headClientId:input.headClientId,members:ids.map(id=>structuredClone(record.members[id]))};
746
775
  }));
747
776
  };
748
777
  return {
@@ -761,7 +790,7 @@ export function createEnrollmentWorkflow({api,discovery,helpers,store,enrollment
761
790
  const records=await store.read(householdScope(input.headClientId));if(!Array.isArray(records))fail('invalid household journal');
762
791
  const matches=records.filter(r=>r.key===input.key);if(matches.length>1)fail('duplicate household journal keys');const record=matches[0];if(!record)return null;
763
792
  if(record.version!==1||!object(record.members))fail('invalid household journal');
764
- return {key:record.key,kind:record.kind,stage:record.stage,householdId:record.context.householdId,headClientId:input.headClientId,members:record.clientIds.map(clientId=>({clientId,key:memberKey(input.key,clientId),...structuredClone(record.members[clientId]??{})}))};
793
+ return {key:record.key,kind:record.kind,stage:record.stage,householdId:record.context.householdId,...(record.context.groupId!==undefined?{groupId:record.context.groupId,householdSource:record.context.householdSource}:{}),headClientId:input.headClientId,members:record.clientIds.map(clientId=>({clientId,key:memberKey(input.key,clientId),...structuredClone(record.members[clientId]??{})}))};
765
794
  });
766
795
  });
767
796
  },
@@ -3,24 +3,33 @@ import {parseRestInstant,toItems} from '../dist/esm/index.js';
3
3
  const positive=v=>Number.isSafeInteger(v)&&v>0;
4
4
  const pairs=members=>JSON.stringify(members.map(({clientId,enrollmentId})=>({clientId,enrollmentId})).sort((a,b)=>a.clientId-b.clientId));
5
5
 
6
- /** Resolves only a unique current household satisfying the adoption membership contract. */
6
+ /** Read the native household association; older custom readers retain explicitly labeled membership matching. */
7
7
  export function createHouseholdEnrollmentDiscovery({api,enrollmentSession,makeError}){
8
8
  const fail=(code,reason)=>{throw makeError(reason,{code,committed:false});};
9
9
  return async input=>{
10
10
  if(!input||Object.keys(input).some(k=>!['headClientId','headEnrollmentId'].includes(k))||!positive(input.headClientId)||!positive(input.headEnrollmentId))fail('HOUSEHOLD_DISCOVERY_INPUT_INVALID','head client and enrollment IDs required');
11
- if(typeof enrollmentSession?.readEnrollment!=='function'||typeof api.households?.listForClient!=='function'||typeof api.households?.get!=='function')fail('HOUSEHOLD_DISCOVERY_READERS_REQUIRED','GWT detail and complete household readers required');
11
+ if(typeof enrollmentSession?.readEnrollment!=='function')fail('HOUSEHOLD_DISCOVERY_READERS_REQUIRED','authoritative enrollment reader required');
12
12
  const target={clientId:input.headClientId,enrollmentId:input.headEnrollmentId};
13
13
  const anchor=await enrollmentSession.readEnrollment(target);
14
14
  const valid=(s,t)=>s?.source==='gwt-rpc'&&s.tenant===api.tenant&&s.clientId===t.clientId&&s.enrollmentId===t.enrollmentId&&positive(s.providerId)&&typeof s.enrollmentType==='string'&&Number.isFinite(parseRestInstant(s.projectStartDate));
15
15
  if(!valid(anchor,target)||!Array.isArray(anchor.members)||!anchor.members.length||anchor.members.some(m=>!positive(m?.clientId)||!positive(m?.enrollmentId))||new Set(anchor.members.map(m=>m.clientId)).size!==anchor.members.length||new Set(anchor.members.map(m=>m.enrollmentId)).size!==anchor.members.length||!anchor.members.some(m=>m.clientId===target.clientId&&m.enrollmentId===target.enrollmentId))fail('HOUSEHOLD_DISCOVERY_DETAIL_INVALID','complete linked enrollment cohort required');
16
16
  if(anchor.members.length===1&&anchor.grouped!==true)fail('ENROLLMENT_NOT_GROUPED','standalone enrollment uses adoptEnrollment');
17
17
  if(anchor.grouped===false&&anchor.members.length>1)fail('HOUSEHOLD_DISCOVERY_DETAIL_INVALID','group flag conflicts with linked cohort');
18
+ const native=anchor.householdId!==undefined,validGroup=v=>Number.isSafeInteger(v)&&v>0||typeof v==='string'&&/^[a-zA-Z0-9_-]+$/.test(v);
19
+ if(native&&(!positive(anchor.householdId)||!validGroup(anchor.groupId)))fail('HOUSEHOLD_DISCOVERY_NATIVE_LINK_REQUIRED','native household and enrollment group IDs required');
18
20
  const members=anchor.members.map(({clientId,enrollmentId})=>({clientId,enrollmentId})).sort((a,b)=>a.clientId-b.clientId),starts=new Map();
19
21
  for(const member of members){
20
22
  const s=member.clientId===target.clientId?anchor:await enrollmentSession.readEnrollment(member);
21
23
  if(!valid(s,member)||s.providerId!==anchor.providerId||s.enrollmentType!==anchor.enrollmentType||!Array.isArray(s.members)||pairs(s.members)!==pairs(members))fail('HOUSEHOLD_DISCOVERY_COHORT_CHANGED','linked enrollment cohort changed during discovery');
24
+ if(native&&(s.householdId!==anchor.householdId||s.groupId!==anchor.groupId||s.grouped===false))fail('HOUSEHOLD_DISCOVERY_COHORT_CHANGED','native household or enrollment group changed during discovery');
22
25
  starts.set(member.clientId,parseRestInstant(s.projectStartDate));
23
26
  }
27
+ if(native){
28
+ const fresh=await enrollmentSession.readEnrollment(target);
29
+ if(!valid(fresh,target)||fresh.householdId!==anchor.householdId||fresh.groupId!==anchor.groupId||fresh.providerId!==anchor.providerId||fresh.enrollmentType!==anchor.enrollmentType||!Array.isArray(fresh.members)||pairs(fresh.members)!==pairs(members))fail('HOUSEHOLD_DISCOVERY_COHORT_CHANGED','native household or enrollment cohort changed during discovery');
30
+ return {...input,householdId:anchor.householdId,householdSource:'native-enrollment',groupId:anchor.groupId,providerId:anchor.providerId,type:anchor.enrollmentType,members};
31
+ }
32
+ if(typeof api.households?.listForClient!=='function'||typeof api.households?.get!=='function')fail('HOUSEHOLD_DISCOVERY_READERS_REQUIRED','legacy readers require complete household readers');
24
33
  const candidates=[],seen=new Set();let total;
25
34
  for(let startPage=1;startPage<=100;startPage++){
26
35
  const result=await api.households.listForClient({path:{clientId:input.headClientId},query:{perPage:100,startPage,total_count:true}});
@@ -17,12 +17,14 @@ export class LoginError extends Error {
17
17
  }
18
18
  export interface UiSession {readonly connection:UiConnection;browser: Browser;context: BrowserContext;page: Page;passwordRecovery:'restored-original'|'not-needed';ensureAuthenticated():Promise<void>;reauthenticate():Promise<void>;close():Promise<void>;}
19
19
  export interface PlaywrightGwtOptions {page:Page;tenant:string;moduleBaseUrl:string;}
20
- export interface PlaywrightEnrollmentSnapshot extends GwtEnrollmentSnapshot {providerId:number;enrollmentType:string;exits:readonly GwtEnrollmentExit[];groupId?:number|string|null;grouped?:boolean;}
20
+ export interface PlaywrightEnrollmentSnapshot extends GwtEnrollmentSnapshot {providerId:number;enrollmentType:string;exits:readonly GwtEnrollmentExit[];groupId?:number|string|null;grouped?:boolean;householdId?:number|null;}
21
21
  export interface EnrollmentListRow {
22
22
  enrollmentId:number;clientId:number;enrollmentProvider:number;type:string;
23
23
  projectStartDate:string;projectExitDate:string|null;isActive:boolean;
24
24
  /** An enrollment group pointer, not a household identifier or member count. */
25
25
  groupId:number|string|null;grouped:boolean;
26
+ /** Native enrollment association, distinct from groupId and current household membership. */
27
+ householdId?:number|null;
26
28
  }
27
29
  export interface EnrollmentListSnapshot {
28
30
  source:'gwt-rpc';tenant:string;clientId:number;count:number;values:EnrollmentListRow[];
@@ -114,11 +114,12 @@ export function createPlaywrightGwtSession({page,tenant,moduleBaseUrl}) {
114
114
  };
115
115
  const listEnrollments=async({clientId})=>{
116
116
  await openReadContext(clientId);
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`);
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, e.householdId AS householdId FROM SpEntryExit AS e WHERE e.active = TRUE AND e.client.active = TRUE AND e.clientId = ${clientId} ORDER BY e.entryExitId`);
118
118
  const ids=new Set();
119
119
  for(const row of rows){
120
120
  if(!Number.isSafeInteger(row.enrollmentId)||row.enrollmentId<=0||row.clientId!==clientId||!Number.isSafeInteger(row.enrollmentProvider)||row.enrollmentProvider<=0||typeof row.type!=='string'||!row.type.trim()||typeof row.projectStartDate!=='string'||!Number.isFinite(Date.parse(row.projectStartDate))||row.projectExitDate!==null&&(typeof row.projectExitDate!=='string'||!Number.isFinite(Date.parse(row.projectExitDate)))||ids.has(row.enrollmentId))throw Error('Invalid enrollment list');
121
121
  ids.add(row.enrollmentId);
122
+ if(row.householdId!==null&&(!Number.isSafeInteger(row.householdId)||row.householdId<=0))throw Error('Invalid enrollment household ID');
122
123
  if(row.groupId!==null){if(typeof row.groupId==='number'&&row.groupId<=0)throw Error('Invalid enrollment group');literal(row.groupId);}
123
124
  }
124
125
  return {source:'gwt-rpc',tenant,clientId,count:rows.length,values:rows.map(row=>({...row,isActive:true,grouped:row.groupId!==null}))};
@@ -137,9 +138,10 @@ export function createPlaywrightGwtSession({page,tenant,moduleBaseUrl}) {
137
138
  };
138
139
  const readEnrollment=async target=>{
139
140
  const model=await openTarget(target),householdClientIds=await householdIds(target);
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)}`);
141
+ const entries=await mapped(`SELECT e.entryExitId AS enrollmentId, e.clientId AS clientId, e.entryDate AS projectStartDate, e.groupId AS groupId, e.householdId AS householdId, 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)}`);
141
142
  if(entries.length!==1)throw Error('Enrollment missing');const entry=entries[0];
142
143
  if(entry.enrollmentId!==target.enrollmentId||entry.clientId!==target.clientId||!Number.isSafeInteger(entry.providerId)||entry.providerId<=0||typeof entry.enrollmentType!=='string'||!entry.enrollmentType.trim())throw Error('Enrollment context incomplete');
144
+ if(entry.householdId!==null&&(!Number.isSafeInteger(entry.householdId)||entry.householdId<=0))throw Error('Invalid enrollment household ID');
143
145
  const members=entry.groupId===null?[{clientId:target.clientId,enrollmentId:target.enrollmentId}]:await mapped(`SELECT e.entryExitId AS enrollmentId, e.clientId AS clientId FROM SpEntryExit AS e WHERE e.active = TRUE AND e.client.active = TRUE AND e.groupId = ${literal(entry.groupId)}`);
144
146
  members.sort((a,b)=>a.clientId-b.clientId);
145
147
  const exitRows=await mapped(`SELECT e.entryExitId AS enrollmentId, e.clientId AS clientId, e.exitDate AS projectExitDate, e.destinationValue.name AS destination, e.destinationValue.code AS destinationCode, e.reasonLeavingValue.name AS reasonForLeaving, e.reasonLeavingValue.code AS reasonForLeavingCode, e.exitSubgroupId AS exitSubgroupId FROM SpEntryExit AS e WHERE e.active = TRUE AND e.client.active = TRUE AND ${entry.groupId===null?`e.entryExitId = ${id(target.enrollmentId)}`:`e.groupId = ${literal(entry.groupId)}`}`);
@@ -165,7 +167,7 @@ export function createPlaywrightGwtSession({page,tenant,moduleBaseUrl}) {
165
167
  reviews.push({reviewId:row.reviewId,type:row.type,reviewDate:row.reviewDate,clientIds:clients.map(c=>c.clientId).sort((a,b)=>a-b)});
166
168
  }
167
169
  reviews.sort((a,b)=>a.reviewId-b.reviewId);
168
- const data={source:'gwt-rpc',tenant,sessionId,...target,projectStartDate:entry.projectStartDate,providerId:entry.providerId,enrollmentType:entry.enrollmentType,groupId:entry.groupId,grouped:entry.groupId!==null,householdClientIds,members,exits,reviews};
170
+ const data={source:'gwt-rpc',tenant,sessionId,...target,projectStartDate:entry.projectStartDate,providerId:entry.providerId,enrollmentType:entry.enrollmentType,groupId:entry.groupId,grouped:entry.groupId!==null,householdId:entry.householdId,householdClientIds,members,exits,reviews};
169
171
  return {...data,revision:digest(model+JSON.stringify({...data,sessionId:undefined}))};
170
172
  };
171
173
  const chooseMembers=async(desired,implicitClientId,dialogButton)=>{
@@ -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"],
@@ -73,7 +73,7 @@ Reproduced on November 2, 2026 in a sandbox tenant with synthetic clients:
73
73
 
74
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.
75
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.
76
- - **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.
77
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.
78
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.
79
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.
@@ -101,7 +101,9 @@ await writeMobileAnswersChecked(api, {
101
101
 
102
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.
103
103
 
104
- 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.
105
107
 
106
108
  ## Race and ethnicity combinations
107
109
 
@@ -96,7 +96,7 @@ 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
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
102
 
@@ -105,3 +105,9 @@ The 0.7.0 label resolver, group flags, household discovery, row registration, er
105
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
106
 
107
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).
108
+
109
+ ## Native enrollment household identity, November 11, 2026
110
+
111
+ Read-only GWT checks of four existing Coordinated Entry groups (eight enrollments) returned both `groupId` and a distinct native `householdId`, including exited groups. The SDK's updated managed reader independently verified six enrollments across three groups. REST complete reads still returned HTTP 400 for the grouped controls, while standalone controls returned null group and household IDs.
112
+
113
+ Using a temporary private journal, native discovery and `adoptHouseholdEnrollment` succeeded for one open and one exited two-person group using only the head's client/enrollment IDs. Both same-key retries returned `already-present`. The workflow was given no household REST reader; no WellSky record-write requests were made. These results verify native reads and read-only adoption, not new live interim or exit writes after household changes. Synthetic tests cover those later operations, identity drift, original-head evidence, current-membership requirements for additions, and legacy-key retries. See [the identity evidence](vendor-issues/enrollment-household-identity.md).
@@ -232,7 +232,7 @@ const detail = await session.enrollmentSession.readEnrollment({
232
232
  // detail.members contains the complete linked clientId/enrollmentId pairs.
233
233
  ```
234
234
 
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.
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. Managed lists and details also return `householdId`, read directly from the enrollment record; `null` means no recorded association. It is optional on custom reader contracts for compatibility. Use `discoverHouseholdEnrollment` below to verify that association across the complete cohort.
236
236
 
237
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
238
 
@@ -262,7 +262,7 @@ For a person in a linked household enrollment, use `exitHousehold` with that per
262
262
 
263
263
  ## Adopt an enrollment entered manually in WellSky
264
264
 
265
- Use `adoptEnrollment({key, clientId, enrollmentId})` for a standalone enrollment. For linked household enrollments, use `adoptHouseholdEnrollment({key, householdId, headClientId, members})`, where each member supplies both `clientId` and `enrollmentId`. Supply the complete linked cohort, including members already exited, and the original enrollment head. Adoption verifies authoritative GWT identities, program/type, entry dates, current household membership, head relationships when present, and head coverage across all enrollment intervals.
265
+ Use `adoptEnrollment({key, clientId, enrollmentId})` for a standalone enrollment. For linked household enrollments, use `adoptHouseholdEnrollment({key, householdId, headClientId, members})`, where each member supplies both `clientId` and `enrollmentId`. Supply the complete linked cohort, including members already exited, and the original enrollment head. Adoption verifies authoritative GWT identities, program/type, entry dates, the native household/group association when available, original head relationships, and head coverage across all enrollment intervals. An explicit household ID must match the native enrollment field. Legacy custom readers without that field retain their current-household validation.
266
266
 
267
267
  Adoption writes only to the local durable journal. It does not create, attach, exit or modify an HMIS record. Later entrants retain their own original entry dates. An enrollment already tracked under another entry key is rejected; use its existing key. Retry an interrupted adoption with the same inputs and store.
268
268
 
@@ -274,13 +274,20 @@ After standalone adoption, its key can be used as `enrollmentKey` for `createInt
274
274
  const discovered = await workflow.discoverHouseholdEnrollment({
275
275
  headClientId, headEnrollmentId,
276
276
  });
277
- // discovered: householdId, householdSource, providerId, type, members
277
+ // Built-in reader: householdId, groupId, householdSource: "native-enrollment", providerId, type, members
278
278
  const adopted = await workflow.adoptHouseholdEnrollment({
279
279
  key: sourceEnrollmentKey, headClientId, headEnrollmentId,
280
280
  });
281
281
  ```
282
282
 
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. 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
+ WellSky stores both `SpEntryExit.groupId` and `SpEntryExit.householdId`. With a built-in reader, discovery reads the household ID directly from the enrollment, verifies that every linked participant has the same household and group IDs, and rechecks the anchor before returning `householdSource: 'native-enrollment'`. It does not search current households to infer the association. Missing native IDs, mixed associations, incomplete cohorts or changed identities stop discovery. A present-but-null native household field never falls back to a membership guess.
284
+
285
+ Native adoption also verifies the original head using entry-time `HUD_RELATIONTOHOH` answers and checks that the head's enrollment covers every member's interval. New native adoptions without that head evidence are refused. Existing verified journal keys retain their accepted head identity and intent hashes. Adoption only records identities locally; it makes no WellSky record writes.
286
+
287
+ Interims and exits for native-linked groups validate the recorded household/group IDs and enrolled participants without requiring them to remain in an active current household. Group identities are pinned in the journal; drift blocks later operations. This allows recorded groups to remain usable after current household membership changes. Enrollment-head replacement remains unsupported, and the original head must still cover the members' enrollment dates. Creation and later member additions continue to require valid current household membership.
288
+
289
+ Legacy custom detail readers that omit `householdId` retain the older unique-current-membership discovery, explicitly labeled `householdSource: 'verified-membership'`. That fallback does not prove a historical association. Upgrade those adapters to return the native `householdId` and `groupId` to use the new path. Existing keys whose original IDs agree remain valid; a conflicting historical guess is refused rather than silently rebound. See [native household evidence](vendor-issues/enrollment-household-identity.md).
290
+
284
291
 
285
292
  ### Register existing entry rows
286
293
 
package/docs/reference.md CHANGED
@@ -37,7 +37,7 @@ Runtime exports: `AttachmentConflictError`, `CoordinatedEntryEventConflictError`
37
37
  | Profile fields and Sex | [Client profile](../browser/client-profile.d.mts) | `read`, `update` |
38
38
  | Calendar DOB | [Birth dates](../browser/date-of-birth.d.mts) | `read`, `validate`, `verify`, `update` |
39
39
  | Provider assessment defaults | [Discovery](../browser/program-discovery.d.mts) | `listAssessments`, `getProviderAssessments` |
40
- | Enrollment and household lifecycle | [Workflow](../browser/enrollment-workflow.d.mts) | Discovery, entry, `correctEntry`, `resolveEntryCorrection`, `adoptEnrollment`, `adoptHouseholdEnrollment`, `discoverHouseholdEnrollment`, `registerEntryRows`, member additions, interim, `exitEnrollment`, `exitHousehold`, journal status; `EntryExitType` and per-type `assessmentLocations` in [enrollments](enrollments.md) |
40
+ | Enrollment and household lifecycle | [Workflow](../browser/enrollment-workflow.d.mts) | Discovery, entry, `correctEntry`, `resolveEntryCorrection`, `adoptEnrollment`, `adoptHouseholdEnrollment`, `discoverHouseholdEnrollment`, `registerEntryRows`, native household/group identity, member additions, interim, `exitEnrollment`, `exitHousehold`, journal status; `EntryExitType` and per-type `assessmentLocations` in [enrollments](enrollments.md) |
41
41
  | Entry-date and exit UI ports | [Member entry](../browser/member-entry-date.d.mts), [exit](../browser/household-exit.d.mts) | Fresh reads and targeted completion |
42
42
  | Services and recovery diagnostics | [Service recording](../browser/service-recording.d.mts) | `record`, `recordBulk`, Funding Sources, `RecoveryDiagnostic` |
43
43
  | SSVF conditional rules | [SSVF types](../browser/ssvf-rules.d.mts) | [Field rules](../SSVF.md) and dated option snapshots |
@@ -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
+ # Enrollment household identity
2
+
3
+ On November 11, 2026, authenticated read-only training checks showed that WellSky exposes both an enrollment group ID and a household ID on `SpEntryExit`. The SDK had selected only the group ID. A group ID must never be substituted for a household ID.
4
+
5
+ The application's current `MetaQueryService` / `GenericService.getMappedResults` accepted a projection of `entryExitId`, `clientId`, `groupId`, `householdId`, provider and enrollment dates. Four existing Coordinated Entry groups, containing eight enrollments, had consistent native household IDs within each group; the household IDs differed from the group IDs. Three of those groups were exited. This establishes the recorded enrollment association without matching current household membership. It does not claim the field is an immutable audit history.
6
+
7
+ The [managed reader](../../browser/playwright-session.mjs) now selects and validates the native field in both catalog and detail reads. SDK reads independently verified six enrollments across three groups. Native discovery and adoption then succeeded on an open group and an exited group, including same-key no-write retries, using a temporary local journal. No enrollment, household, assessment, review, exit or password was changed. Raw captures and client identifiers are excluded from the package.
8
+
9
+ The pinned REST schema already declares `householdId`, but grouped complete reads returned HTTP 400 in the read-only controls. Standalone control reads succeeded with null household and group IDs. The existing [grouped REST defect](enrollment-effective-date-column.md) still applies.
10
+
11
+ Native adoption verifies the complete cohort's recorded household/group identities and entry-time head relationship. Reviews and exits retain the original enrollment head and interval coverage while using the native association. Current household membership continues to constrain creation and member additions. A missing native field in an older custom reader retains the explicitly labeled legacy path; a null or inconsistent native field is not guessed from current membership.
12
+
13
+ See [adoption and discovery](../enrollments.md#discover-a-household-from-its-head-enrollment) for usage and compatibility limits.
@@ -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?
@@ -1,6 +1,6 @@
1
1
  import {createEnrollmentWorkflow,type EnrollmentWorkflowOptions,type EntryCorrectionInput} from '@choiceapps/communityservices-sdk/browser';
2
2
 
3
- /** The head's explicit target identifies a cohort; ambiguous household matches fail. */
3
+ /** Built-in readers verify the native household and group; older adapters identify their membership fallback explicitly. */
4
4
  export async function adoptFromHead(dependencies:EnrollmentWorkflowOptions,input:{key:string;headClientId:number;headEnrollmentId:number}){
5
5
  const workflow=createEnrollmentWorkflow(dependencies);
6
6
  const discovery=await workflow.discoverHouseholdEnrollment({headClientId:input.headClientId,headEnrollmentId:input.headEnrollmentId});
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@choiceapps/communityservices-sdk",
3
- "version": "0.8.0",
3
+ "version": "0.9.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",