@choiceapps/communityservices-sdk 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +0 -17
  3. package/browser/coordinated-entry-event.d.mts +78 -0
  4. package/browser/coordinated-entry-event.mjs +175 -0
  5. package/browser/current-living-situation.mjs +11 -120
  6. package/browser/enrollment-workflow.d.mts +17 -3
  7. package/browser/enrollment-workflow.mjs +78 -17
  8. package/browser/index.d.mts +1 -0
  9. package/browser/index.mjs +1 -0
  10. package/browser/subassessment-row-recorder.mjs +154 -0
  11. package/dist/cjs/encoding.d.ts.map +1 -1
  12. package/dist/cjs/encoding.js +2 -1
  13. package/dist/cjs/encoding.js.map +1 -1
  14. package/dist/cjs/errors.d.ts +4 -1
  15. package/dist/cjs/errors.d.ts.map +1 -1
  16. package/dist/cjs/errors.js +9 -1
  17. package/dist/cjs/errors.js.map +1 -1
  18. package/dist/cjs/runtime.d.ts +5 -1
  19. package/dist/cjs/runtime.d.ts.map +1 -1
  20. package/dist/cjs/runtime.js +1 -1
  21. package/dist/cjs/runtime.js.map +1 -1
  22. package/dist/cjs/subassessments.d.ts.map +1 -1
  23. package/dist/cjs/subassessments.js +8 -15
  24. package/dist/cjs/subassessments.js.map +1 -1
  25. package/dist/cjs/transport.d.ts.map +1 -1
  26. package/dist/cjs/transport.js +15 -3
  27. package/dist/cjs/transport.js.map +1 -1
  28. package/dist/esm/encoding.d.ts.map +1 -1
  29. package/dist/esm/encoding.js +2 -1
  30. package/dist/esm/encoding.js.map +1 -1
  31. package/dist/esm/errors.d.ts +4 -1
  32. package/dist/esm/errors.d.ts.map +1 -1
  33. package/dist/esm/errors.js +9 -1
  34. package/dist/esm/errors.js.map +1 -1
  35. package/dist/esm/runtime.d.ts +5 -1
  36. package/dist/esm/runtime.d.ts.map +1 -1
  37. package/dist/esm/runtime.js +1 -1
  38. package/dist/esm/runtime.js.map +1 -1
  39. package/dist/esm/subassessments.d.ts.map +1 -1
  40. package/dist/esm/subassessments.js +8 -15
  41. package/dist/esm/subassessments.js.map +1 -1
  42. package/dist/esm/transport.d.ts.map +1 -1
  43. package/dist/esm/transport.js +15 -3
  44. package/dist/esm/transport.js.map +1 -1
  45. package/docs/README.md +1 -0
  46. package/docs/assessments.md +21 -3
  47. package/docs/capabilities.md +2 -0
  48. package/docs/configuration.md +1 -0
  49. package/docs/coordinated-entry-events.md +88 -0
  50. package/docs/current-living-situation.md +1 -1
  51. package/docs/dates-and-answers.md +3 -1
  52. package/docs/enrollments.md +41 -1
  53. package/docs/reference.md +6 -2
  54. package/docs/troubleshooting.md +25 -0
  55. package/docs/using-the-sdk.md +2 -3
  56. package/docs/vendor-issues/README.md +2 -0
  57. package/docs/vendor-issues/date-semantics.md +2 -0
  58. package/docs/vendor-issues/yes-no-answers.md +35 -0
  59. package/examples/README.md +2 -0
  60. package/examples/coordinated-entry-events.mts +37 -0
  61. package/examples/enrollment-workflow.mts +5 -2
  62. package/package.json +1 -1
@@ -0,0 +1,88 @@
1
+ # Record Coordinated Entry Events
2
+
3
+ [Documentation index](README.md) · [Assessment guide](assessments.md) · [Public contract](../browser/coordinated-entry-event.d.mts) · [Example](../examples/coordinated-entry-events.mts)
4
+
5
+ A Coordinated Entry Event records an access event, such as a referral to a crisis needs assessment, or a referral with its result, such as an emergency shelter or rapid rehousing referral. Each event is its own subassessment row. Recording another event does not close or replace an earlier one.
6
+
7
+ `createCoordinatedEntryEventRecorder` writes one row per event key through the standard assessment endpoint. The mobile answer endpoint is not used because it drops two kinds of answers in these rows without returning an error; see [the evidence](#why-the-standard-endpoint).
8
+
9
+ ## Fields
10
+
11
+ Read `api.assessments.getDefinition({clientId, assessmentCode, refresh: true})` and supply the exact virtual fields. Readback queries one second after the later of now and the event date, because a read at exactly the event instant omits yes/no answers. A November 2, 2026 sandbox instrument exposed:
12
+
13
+ | Role | Virtual field | Type |
14
+ | --- | --- | --- |
15
+ | Parent | `SVP_COORDINATEDENTRYEVENT_SUBA` | sub_assessment |
16
+ | Start | `SVP_COORDINATEDENTRYEVENT_SUBASTART` | date |
17
+ | End | `SVP_COORDINATEDENTRYEVENT_SUBAEND` | date |
18
+ | Date of event | `SVP_COORDINATEDENTRYEVENT_DATE_EVENT` | date |
19
+ | Event | `SVP_COORDINATEDENTRYEVENT_EVENT` | lookup |
20
+ | Diversion result: client housed safely | `SVP_COORDINATEDENTRYEVENT_CLIENTHOUSEDSAFE` | yes_no |
21
+ | Follow-up result: enrolled in aftercare | `SVP_COORDINATEDENTRYEVENT_ENROLLEDAFTERCARE` | yes_no |
22
+ | Location of crisis or permanent housing referral | `SVP_COORDINATEDENTRYEVENT_LOCATIONCRISISPHPROJID` | provider |
23
+ | Referral result | `SVP_COORDINATEDENTRYEVENT_REFERRALRESULT` | lookup |
24
+ | Date of result | `SVP_COORDINATEDENTRY_DATE_RESULT` | date |
25
+
26
+ Map every role in `fields`. The recorder writes the start, event date and event, and takes the follow-up answers as named inputs. It checks them against the selected Event before any write:
27
+
28
+ | Event option code (`COORDINATEDENTRYEVENT_EVENT_…`) | `housedSafely` | `enrolledInAftercare` | `location` | `referralResult` + `resultDate` |
29
+ | --- | --- | --- | --- | --- |
30
+ | `PROBSOLV` | Optional | — | — | — |
31
+ | `POSTPLACEFOLLOWUP` | — | Optional | — | — |
32
+ | `EMERGSHELTER`, `TRANSITHOUSING`, `JOINTTHRRH`, `RRH`, `PSH`, `OTHERPH` | — | — | **Required** | Optional, both or neither |
33
+ | `PREVASSIST`, `CRISISNEEDS`, `HOUSINGNEEDS`, `STREETOUTREACH`, `HOUSINGNAVIGATION`, `NONCONT_INELIG`, `NONCONT_NOAVAIL` | — | — | — | — |
34
+
35
+ A dash means the answer is refused for that event. The mapping follows the form's own dependency labels for these fields. Requiring a location for placement referrals is the SDK's choice; HUD's data standards list it as a dependent field to collect. HUD also defines referral events for emergency assistance or flex funds, Emergency Housing Vouchers and Housing Stability Vouchers, which take a result but no location. The tested tenant's Event picklist had no options for them, so they are not in the table; their codes are refused as `unsupported-event` until verified and added. The result answers are optional because a referral's result is usually known only after the referral, and the result date cannot precede the event. Header options and any other Event code are refused as `unsupported-event`, so a tenant with different codes fails before writing rather than being guessed. Supply `location` as a provider ID; the recorder checks that it is a positive ID and that it reads back, not that the provider exists in the tenant. Use `additionalAnswers` only for other questions in the row; it cannot set the mapped fields. Malformed answers and lookups without a `writeLabel` result are rejected with the encoder's `ValidationError` before any write.
36
+
37
+ ## Record and retry
38
+
39
+ ```ts
40
+ const recorder = createCoordinatedEntryEventRecorder({
41
+ api, store, fields,
42
+ writeLabel: (question, option) => verifiedLabels.get(`${question.fieldName}:${option.code}`),
43
+ });
44
+ await recorder.record({
45
+ key: `${sourceEventId}`, clientId, enrollmentId, assessmentCode,
46
+ eventDate: '2026-11-02T12:20:00Z',
47
+ event: {option: {code: 'COORDINATEDENTRYEVENT_EVENT_EMERGSHELTER'}},
48
+ location: shelterProviderId,
49
+ referralResult: {option: {code: 'COORDINATEDENTRYEVENT_REFERRALRESULT_SUCCESS'}},
50
+ resultDate: '2026-11-02T12:30:00Z',
51
+ });
52
+ ```
53
+
54
+ - The standard endpoint takes lookup **labels**, so `writeLabel` must return tenant-verified labels for the Event, the referral result and any other lookup answered. Readback compares the option IDs.
55
+ - The row start is the selected enrollment's entry date. The end is left empty. Events before entry or after a recorded exit are refused.
56
+ - The standard endpoint rejects a request that omits any mandatory answer of the instrument. The recorder sends only the event row, so it refuses an instrument with mandatory questions instead of resending other answers, which would create new dated rows for them. Use an instrument that contains the CE Event subassessment and has no mandatory questions; in the tested program that was the Standard interim instrument. Rows belong to the client, so they appear in the entry and interim assessments alike.
57
+ - The journal, snapshot and reconciliation rules match the [Current Living Situation recorder](current-living-situation.md#record-and-retry): each key gets one row, earlier rows must be unchanged, exactly one new matching row must appear, and an uncertain save blocks later event keys for that client until it reconciles. Same-key retries verify the original recordset ID and never submit again.
58
+
59
+ Use timezone-qualified whole-second instants for event and result dates.
60
+
61
+ ## Add a referral result later
62
+
63
+ ```ts
64
+ await recorder.recordResult({
65
+ key: `${sourceEventId}`, clientId, assessmentCode,
66
+ referralResult: {option: {code: 'COORDINATEDENTRYEVENT_REFERRALRESULT_SUCCESS'}},
67
+ resultDate: '2026-11-05T15:00:00Z',
68
+ });
69
+ ```
70
+
71
+ `recordResult` adds a result to a placement referral that `record` already verified under the same key and store. The standard endpoint can only create rows, so it sends just the result and result date to the event's `recordset_id` through the mobile endpoint, which changes those cells and keeps the rest of the row, including the location. Before sending, it journals a snapshot of every row. Afterwards it requires the result to read back, every earlier answer in the row to be unchanged, every other row to be unchanged and no row to be added. A lost acknowledgement reconciles; an unknown outcome leaves the update pending, blocks other event keys for the client, and is never resent. A row that already has the same result returns `already-present`; a different result already in the row is a `result-conflict`. Once a result has been attempted or recorded under a key, a different result for that key is refused as `result-intent-changed`. The result date cannot precede the event date; it may follow the client's exit, because a referral's outcome can arrive after the client leaves the program. Mobile writes use the client's configured date correction for their effective date; the result date inside the row is not shifted.
72
+
73
+ Yes/no results cannot be added later: the mobile endpoint drops yes/no answers in row updates as it does in new rows. Record a Problem Solving/Diversion or post-placement follow-up event once its result is known, with `housedSafely` or `enrolledInAftercare`, or record it without one. The standard endpoint stores the requested instant; the mobile date correction does not apply.
74
+
75
+ ## Why the standard endpoint
76
+
77
+ On November 2, 2026, synthetic CE Event rows were written in a sandbox tenant through both endpoints:
78
+
79
+ | Row | Mobile `AddAssessmentAnswer` | Standard `assessments.create` |
80
+ | --- | --- | --- |
81
+ | Dates and event lookup | Saved | Saved |
82
+ | Yes/no result | Dropped, success response | Saved |
83
+ | Provider location | Dropped, success response | Saved |
84
+ | Referral result and date | Saved | Saved |
85
+
86
+ The standard request uses the row shape `{subAssessments: [...]}` without `recordset_id`/`isDeleted`. Other subassessment rows were unchanged afterwards. The recorder, before its event-type checks were added, then created an access event, a diversion with its yes/no result, and an emergency shelter referral with location and result through the interim instrument; all three verified and same-key retries made no writes. The UI's assessment table showed the referral's location and result. A mobile update of an existing row's `recordset_id` with only the result and result date added both and kept the location, with or without the location cell in the request; the same update with a yes/no result returned success and stored nothing. The yes/no result is not a table column, so it was confirmed through REST reads only.
87
+
88
+ Apart from adding a referral result, the recorder does not edit or delete events. End-date an event row with [`client.subassessments.setEndDate`](assessments.md#end-date-an-existing-subassessment) and the recorded `recordsetId`; it sends only the end-date cell, keeps the location and yes/no results, and verifies the whole subassessment. A later end date does not stop `record` from returning `already-present` for the event's key.
@@ -41,7 +41,7 @@ Each input specifies an existing client and enrollment, the instrument, a stable
41
41
 
42
42
  As with the existing enrollment workflow, date inputs use timezone-qualified ISO instants with whole-second precision. Supply the contact's intended date and time in the program's timezone, for example `2026-10-24T09:00:00-04:00`. The information-date answer and assessment effective-date query use that instant; the row start uses program entry. The helper does not use the host timezone or shift body dates. Only a separately configured, measured mobile query correction affects the query; see [date contracts](dates-and-answers.md). Date-only input is rejected rather than interpreted in an unknown timezone.
43
43
 
44
- For outreach, record each meaningful outreach contact with its own upstream event key and the situation reported that day. For night-by-night shelter, record each meaningful contact with a separate key; do not assume a bed-night automatically supplies a contact or the client's answer. Two distinct contact keys create two rows even if the date and answers are identical. Same-key retries verify the original recordset ID and never submit again.
44
+ For outreach, record each meaningful outreach contact with its own upstream event key and the situation reported that day. For night-by-night shelter, record each meaningful contact with a separate key; do not assume a bed-night automatically supplies a contact or the client's answer. Two distinct contact keys create two rows even if the date and answers are identical. Same-key retries verify the original recordset ID and never submit again. If a contact row is later end-dated, a same-key retry reports `verified-contact-changed`, because the recorder verifies the open end it wrote.
45
45
 
46
46
  The recorder snapshots all existing CLS rows, journals the attempt before submitting one new `recordset_id: 0`, then requires exactly one additional matching row and preservation of earlier rows. It does not adopt a preexisting row merely because its answers match. Lost responses reconcile through the journal and fresh reads. Missing, changed, multiple, or ambiguous new rows leave the attempt unresolved, blocking resubmission and later contact keys for that client. Restore the same journal and reconcile the original operation; never clear it or generate a replacement key to force a retry.
47
47
 
@@ -15,7 +15,9 @@ The SDK deals with several kinds of dates. Applying one correction to all of the
15
15
  | Goal case-note date | `YYYY-MM-DD` | Browser-local calendar date |
16
16
  | Service dates and financial-assistance period | Explicit timestamps | Separate fields; the period is not inferred from the service dates |
17
17
 
18
- The observed seven-hour mobile effective-date shift and DOB conversion are separate server behaviors. `mobileAssessmentDateCorrectionMinutes: 420` compensates for a measured subtraction in one mobile query. It does not fix DOB values or verification array filtering. Never pre-adjust a timestamp and also enable the correction. Revalidate measured offsets against saved effective dates after deployment/server changes.
18
+ The observed seven-hour mobile effective-date shift and DOB conversion are separate server behaviors. Both match WellSky servers running on Mountain Standard Time (UTC−7, no daylight saving), as the project maintainers report; WellSky has not documented it as a contract. Standard `assessments.create` writes keep the requested instant and must not use the correction. `mobileAssessmentDateCorrectionMinutes: 420` compensates for a measured subtraction in one mobile query. It does not fix DOB values or verification array filtering. Never pre-adjust a timestamp and also enable the correction. Revalidate measured offsets against saved effective dates after deployment/server changes.
19
+
20
+ An undated generic assessment read returns the server's current view, which omits rows dated after the server's current time. On November 2, 2026, rows written for a November 5 interim were missing from an undated read and present in reads dated November 5. The enrollment workflow reads at the later of now and the record's date. The Current Living Situation and Coordinated Entry Event recorders read one second after that, because a read at exactly a row's instant omits its yes/no answers.
19
21
 
20
22
  The DOB helper verifies client API, assessment effective date, and fresh UI values. At a new effective timestamp it uses REST. Correcting the latest existing timestamp requires the matching entry assessment context because the tested REST path returns an entity-tracking error. The helper does not move effective dates to avoid that error. Older history correction and same-timestamp interim/exit correction are not supported.
21
23
 
@@ -27,7 +27,26 @@ REST form metadata includes question paths, mandatory flags, current option IDs/
27
27
 
28
28
  ## Enrollment workflow from program discovery
29
29
 
30
- `createEnrollmentWorkflow` composes the provider catalog, current program assessment defaults, question/picklist metadata, REST enrollment and answer writes, and the durable GWT interim helper. It accepts an **existing client ID** and supports HUD and VA program defaults. It does not create clients or infer program eligibility, answers, conditional applicability, or which verification types a program requires.
30
+ `createEnrollmentWorkflow` composes the provider catalog, current program assessment defaults, question/picklist metadata, REST enrollment and answer writes, and the durable GWT interim helper. It accepts an **existing client ID**. It does not create clients or infer program eligibility, answers, conditional applicability, or which verification types a program requires.
31
+
32
+ The workflow's `EntryExitType` accepts `HUD`, `Basic`, `Standard`, `RHY`, `VA`, `Basic Center Program Entry/Exit`, `PATH`, `Quick Call`, and `Transitional Living Program Entry/Exit`. `type` identifies the Entry/Exit category; `assessmentCode` identifies its assessment instrument. The workflow sends the selected type unchanged to REST. The generated REST methods accept a string; neither contract guarantees that the server permits every type for a given provider.
33
+
34
+ HUD and VA retain built-in provider display locations. For another type, supply `assessmentLocations` when constructing the workflow. First inspect `getProviderAssessments({providerId}).defaults`, including each item's `label`, then map the desired type's entry, interim, and optional exit phases to their exact `location` values. These values are display-control identifiers, not assessment names, IDs, or codes. The workflow resolves the instruments from fresh provider discovery on every operation. It rejects missing, duplicate, unset, or unresolved defaults before record writes and never falls back to another type.
35
+
36
+ ```ts
37
+ const workflow = createEnrollmentWorkflow({
38
+ api, discovery, helpers, store,
39
+ assessmentLocations: {
40
+ PATH: {
41
+ entry: selectedPathEntry.location,
42
+ interim: selectedPathInterim.location,
43
+ exit: selectedPathExit.location,
44
+ },
45
+ },
46
+ });
47
+ ```
48
+
49
+ `assessmentLocations` is a partial map keyed by `EntryExitType`. Each supplied mapping requires nonempty `entry` and `interim` locations; `exit` is required for `discoverExit` and household exits. A supplied HUD or VA mapping replaces that type's built-in mapping. Keep the same configuration across retries and use separate workflow instances when providers need different location mappings. Existing journal checks still reject changed instruments or definitions. Additional types have synthetic workflow coverage, not new live acceptance evidence. The November 2, 2026 report of type rejection was traced to the SDK's former HUD/VA restriction, not a reproduced WellSky server failure.
31
50
 
32
51
  ```ts
33
52
  import {
@@ -88,6 +107,27 @@ See the [packaged enrollment example](../examples/enrollment-workflow.mts) for a
88
107
 
89
108
  [Live workflow acceptance](capabilities.md) covers a new synthetic client, exact entry/interim UI values, all verification table pages, and repeated calls with zero REST writes or GWT creations. [DOB workflow acceptance](capabilities.md) covers a leap-day DOB, distinct household member DOBs, a later-entry newborn, and repeated entry/interim operations after later assessment changes.
90
109
 
110
+ ### Yes/no answers
111
+
112
+ Mobile answer writes store yes/no values empty (see [yes/no acceptance](assessments.md#yesno-answer-acceptance)). With the default `yesNoWrites: 'mobile'`, a plan that answers a yes/no question leaves its `scalars` stage pending. To save those answers, construct the workflow with the standard endpoint:
113
+
114
+ ```ts
115
+ const workflow = createEnrollmentWorkflow({
116
+ api, discovery, helpers, store,
117
+ yesNoWrites: 'standard',
118
+ writeLabel: (question, option) => verifiedLabels.get(`${question.fieldName}:${option.code}`),
119
+ });
120
+ ```
121
+
122
+ In this mode the mobile batch carries no yes/no answers. After it verifies, a separate `yesno` stage sends every yes/no answer to `assessments.create` together with each mandatory top-level answer, as that endpoint requires:
123
+
124
+ - Only mandatory lookups are supported, matching the live evidence. Any other mandatory type, including DOB, fails before any write.
125
+ - A mandatory lookup in `answers` is sent with its `writeLabel` label.
126
+ - A mandatory lookup in `notApplicable` is sent as `""` and stored empty at the assessment date. Blank acceptance was reproduced for a conditionally mandatory lookup only. The workflow refuses this before any write when the field already has a value at that date; answer it explicitly instead.
127
+ - A mandatory lookup in `carryForward` is resent with the label of its recorded option. Unlike ordinary carry-forward, this writes a new row for it at the assessment date.
128
+
129
+ Every mandatory label is resolved before the first write. The `yesno` stage verifies with a generic read one second after the effective instant, because a read at the instant omits yes/no answers, and requires each answer and its effective timestamp to match. The mode is recorded in the journal. An unfinished workflow started in one mode fails in the other rather than switching endpoints, including journals left pending by earlier mobile yes/no attempts; those require separate reconciliation. Yes/no children of new subassessment rows still use the mobile row write, which drops them, so leave them unanswered in workflow rows. Record Coordinated Entry Events, which need yes/no and provider answers, with the [event recorder](coordinated-entry-events.md).
130
+
91
131
  ## Household enrollment and interim assessments
92
132
 
93
133
  The same workflow exposes `createHouseholdEnrollment` and `createHouseholdInterim`. Supply `enrollmentSession: session.enrollmentSession` when constructing it. Initial household enrollment requires an existing household and at least two existing clients, including its head. Later interims and exits accept a nonempty subset of enrolled members. Establish household membership separately before enrollment.
package/docs/reference.md CHANGED
@@ -25,7 +25,7 @@ Runtime exports at the root: `AmbiguousCommitError`, `AssessmentMetadata`, `Auth
25
25
 
26
26
  Import from `@choiceapps/communityservices-sdk/browser` in server-side ESM. These helpers use the tenant supplied by your application. Browser interfaces require `playwright-core`; some orchestration helpers use REST plus injected UI ports rather than directly launching a browser.
27
27
 
28
- Runtime exports: `AttachmentConflictError`, `EnrollmentWorkflowError`, `LoginError`, `SessionInterruptedError`, `createFileGwtMutationStore`, `createPlaywrightEnrollmentHelpers`, `createPlaywrightGwtSession`, `createCasePlanUi`, `createCasePlans`, `createClientCreator`, `createClientProfile`, `createClientProfileUi`, `createClientWorkflows`, `createDateOfBirth`, `createDateOfBirthUi`, `createEnrollmentWorkflow`, `createHouseholdExitUi`, `createHouseholdUi`, `createHouseholds`, `createMemberEntryDatePort`, `createProgramDiscovery`, `createServiceRecorder`, `createServiceUi`, `getHouseholdOptions`, `getSsvfServiceRules`, `openEnrollmentSession`, `openUiSession`, `validateBirthDate`, `resolveUiConnection`.
28
+ Runtime exports: `AttachmentConflictError`, `CoordinatedEntryEventConflictError`, `EnrollmentWorkflowError`, `LoginError`, `SessionInterruptedError`, `createFileGwtMutationStore`, `createPlaywrightEnrollmentHelpers`, `createPlaywrightGwtSession`, `createCasePlanUi`, `createCasePlans`, `createClientCreator`, `createClientProfile`, `createClientProfileUi`, `createClientWorkflows`, `createCoordinatedEntryEventRecorder`, `createDateOfBirth`, `createDateOfBirthUi`, `createEnrollmentWorkflow`, `createHouseholdExitUi`, `createHouseholdUi`, `createHouseholds`, `createMemberEntryDatePort`, `createProgramDiscovery`, `createServiceRecorder`, `createServiceUi`, `getHouseholdOptions`, `getSsvfServiceRules`, `openEnrollmentSession`, `openUiSession`, `validateBirthDate`, `resolveUiConnection`.
29
29
 
30
30
  | Task | Contract | Methods or behavior |
31
31
  | --- | --- | --- |
@@ -36,7 +36,7 @@ Runtime exports: `AttachmentConflictError`, `EnrollmentWorkflowError`, `LoginErr
36
36
  | Profile fields and Sex | [Client profile](../browser/client-profile.d.mts) | `read`, `update` |
37
37
  | Calendar DOB | [Birth dates](../browser/date-of-birth.d.mts) | `read`, `validate`, `verify`, `update` |
38
38
  | Provider assessment defaults | [Discovery](../browser/program-discovery.d.mts) | `listAssessments`, `getProviderAssessments` |
39
- | Enrollment and household lifecycle | [Workflow](../browser/enrollment-workflow.d.mts) | Discovery, entry, member additions, interim, exit, journal status |
39
+ | Enrollment and household lifecycle | [Workflow](../browser/enrollment-workflow.d.mts) | Discovery, entry, member additions, interim, exit, journal status; `EntryExitType` and per-type `assessmentLocations` in [enrollments](enrollments.md) |
40
40
  | 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 |
41
41
  | Services and recovery diagnostics | [Service recording](../browser/service-recording.d.mts) | `record`, `recordBulk`, Funding Sources, `RecoveryDiagnostic` |
42
42
  | SSVF conditional rules | [SSVF types](../browser/ssvf-rules.d.mts) | [Field rules](../SSVF.md) and dated option snapshots |
@@ -62,3 +62,7 @@ See [shelter contracts](../browser/shelters.d.mts), [shelter workflow](shelters.
62
62
  ### Current Living Situation
63
63
 
64
64
  `createCurrentLivingSituationRecorder` and `CurrentLivingSituationConflictError`: [contact guide](current-living-situation.md), [declarations](../browser/current-living-situation.d.mts). `record` creates one journaled row per meaningful contact and verifies exact dates, answers, and recordset identity.
65
+
66
+ ### Coordinated Entry Events
67
+
68
+ `createCoordinatedEntryEventRecorder` and `CoordinatedEntryEventConflictError`: [event guide](coordinated-entry-events.md), [declarations](../browser/coordinated-entry-event.d.mts). `record` creates one journaled CE Event row per event key through the standard assessment endpoint and verifies every answer, including provider locations and yes/no results.
@@ -43,6 +43,31 @@ Successful adoption of an existing record also needs its journal. Retain it even
43
43
 
44
44
  Not every browser helper error is a `WellSkyError`; some validation/UI failures are ordinary `Error` objects. `verifyWrite` returns a discriminated result rather than using a thrown error as its success contract. Its `unverified` result may already be committed.
45
45
 
46
+ ## Inspect an upstream error privately
47
+
48
+ By default the transport discards HTTP error bodies because they can contain client data or credentials. For a controlled diagnostic session, construct `WellSkyClient` with `captureErrorBody: true`. An `UpstreamError` then exposes `responseBody` and `responseBodyTruncated`. The body is capped at 16,384 characters, retained in memory, and excluded from the error's enumerable properties, JSON serialization, and transport hooks/logger events. HTTP 401, 403, 429 and SetAuthKey authentication responses are excluded even with capture enabled. Raw bodies can still contain private information; inspect them through an access-controlled debugger or application diagnostic sink, not ordinary logs.
49
+
50
+ For an enrollment write that fails and cannot be verified, `EnrollmentWorkflowError.step` identifies the durable stage and `cause` preserves the original write error. If the post-write reconciliation read also fails, `cause` is an `AggregateError` containing the write and read errors. Causes and response bodies are not written to the durable journal and are not recoverable from a later process or retry. A write that verifies successfully remains successful even if its response reported failure.
51
+
52
+ ```ts
53
+ import {UpstreamError} from '@choiceapps/communityservices-sdk';
54
+ import {EnrollmentWorkflowError} from '@choiceapps/communityservices-sdk/browser';
55
+
56
+ // api was constructed with captureErrorBody: true for this diagnostic session.
57
+ try {
58
+ await workflow.createEnrollment(originalInput);
59
+ } catch (error) {
60
+ const upstream = error instanceof EnrollmentWorkflowError ? error.cause : error;
61
+ if (upstream instanceof UpstreamError) {
62
+ // Inspect upstream.status, requestId, correlationId, responseBody and
63
+ // responseBodyTruncated privately. Sanitize before sharing any excerpt.
64
+ }
65
+ throw error;
66
+ }
67
+ ```
68
+
69
+ Enabling capture does not authorize another write or recover an already discarded body. A previously attempted stage only reconciles on retry; it does not submit again to obtain diagnostics. Preserve the original journal and arrange a separately authorized, minimal reproduction when a new response is needed. Never switch to another endpoint after an uncertain mutation merely to see whether it succeeds.
70
+
46
71
  ## Check unexpected values
47
72
 
48
73
  - Empty complete-enrollment verification arrays: use [assessment-based snapshots](assessments.md#read-verification-records-at-enrollment). Do not shift stored dates.
@@ -6,7 +6,7 @@ This guide takes an application from installation to its first REST read and bro
6
6
 
7
7
  ## 1. Install the package
8
8
 
9
- Install from the public npm registry. No registry token is required. For the private registry, follow [Install from GitLab](../README.md#install-from-gitlab).
9
+ Install from the public npm registry. No registry token is required.
10
10
 
11
11
  ```sh
12
12
  npm install @choiceapps/communityservices-sdk
@@ -46,11 +46,10 @@ This runs the real SDK validation and transport through a fake that cannot conta
46
46
 
47
47
  The application supplies the tenant. Every tenant uses the same SDK operations; no environment switch or separate helper names are needed. Tenant IDs, provider IDs, client IDs, and picklist choices must come from the deployment you are connecting to.
48
48
 
49
- REST and browser credentials are separate. A registry token is only needed if you choose the private GitLab registry:
49
+ REST and browser credentials are separate:
50
50
 
51
51
  | Credential | Used for |
52
52
  | --- | --- |
53
- | `GITLAB_NPM_TOKEN` | Downloading the package from GitLab |
54
53
  | REST username/password, token, or token provider | Authenticating `WellSkyClient` API requests |
55
54
  | `HMIS_UI_USER` and `HMIS_UI_PASSWORD` | Signing in to the web application through a browser helper |
56
55
 
@@ -10,6 +10,7 @@ This is a feedback queue prepared for WellSky. It does not claim that these repo
10
10
 
11
11
  | Issue | Status | Impact |
12
12
  | --- | --- | --- |
13
+ | [Mobile writes store yes/no answers empty](yes-no-answers.md) | Reproduced November 2, 2026, with a fresh-client control | Answers report success but save nothing; a mobile retry gets HTTP 400 because the empty row exists. The standard endpoint works but requires every mandatory answer. |
13
14
  | [Empty service collection returns HTTP 404](empty-service-list-404.md) | V3 reproduced October 24, 2026 | An existing client with zero services cannot be recognized as an empty collection from the REST status alone. |
14
15
  | [Grouped enrollment reads report a missing database column](enrollment-effective-date-column.md) | V2 and V3 compared October 24, 2026 | All 16 tested grouped complete reads per version failed; individual enrollment pages in the same client could succeed. V2 is not a workaround. |
15
16
  | [Missing DOB information triggers a complete-read target error](enrollment-complete-target-error.md) | V2 and V3 reproduced October 24, 2026 | DOB alone or unknown/declined DOB quality alone cleared the error in four synthetic fixtures; no birth date is needed for the tested quality responses. |
@@ -30,6 +31,7 @@ The enrollment report includes a read-only reproduction using existing synthetic
30
31
  - [Client update semantics](write-contracts.md#omitting-ssn-from-a-client-update-cleared-it): omitting SSN cleared its value. Clarify replacement versus partial-update behavior before classifying the omission rule as a defect.
31
32
  - [Goal update and relationship contract](write-contracts.md#goal-updates-require-a-field-marked-optional): `clientId` was required despite an optional schema field; returned household rows did not match the inspected goal association.
32
33
  - [Mobile effective dates](date-semantics.md#mobile-answer-writes-shift-the-effective-date) and [DOB serialization](date-semantics.md#dob-serialization-can-disagree-with-the-ui-calendar-date): document timezone/date-only rules and resolve inconsistent stored or displayed values. These are separate from complete-entry filtering.
34
+ - [Yes/no answer contract](yes-no-answers.md): mobile wire format for yes/no, insert-only mobile writes, mandatory answers on standard updates, and the generic read's effective-instant comparison.
33
35
  - [Filtering, empty reads, and response shapes](write-contracts.md#filtering-and-response-shapes-vary-by-operation): document per-endpoint syntax and align responses with schemas.
34
36
  - [Household head coverage](household-head-validation.md): the server accepted writes that left members enrolled after the original head exited. Clarify the intended validation and supported head-transfer/repair workflow; the SDK rejects that state.
35
37
 
@@ -28,6 +28,8 @@ The alternative enrollment-scoped assessment read included future-start rows as
28
28
 
29
29
  In the controlled fixture, an AddAssessmentAnswer request with `dateEffective=2026-10-18T16:00:00Z` stored a child effective date of `2026-10-18T09:00:00`. Sending `23:00:00Z` through a measured 420-minute correction stored the intended `16:00:00` value. Body row-start dates were controlled independently and retained their intended instants.
30
30
 
31
+ Suspected cause: the project maintainers report that WellSky servers run on Mountain Standard Time (UTC−7, no daylight saving), which matches the constant offset in winter and summer.
32
+
31
33
  **Ask WellSky:** Is `dateEffective` interpreted as UTC, a local wall time, or another representation? Can the API preserve an explicit offset and document its response timezone? Is the observed subtraction intended?
32
34
 
33
35
  The SDK exposes `mobileAssessmentDateCorrectionMinutes` for this measured query behavior only. It is not a global date shift and does not fix the complete-entry projection.
@@ -0,0 +1,35 @@
1
+ # Yes/no answers through assessment endpoints
2
+
3
+ [WellSky feedback](README.md)
4
+
5
+ Status: reproduced November 2, 2026 in a sandbox tenant on synthetic clients, with private HTTP error bodies captured. A coordinated-entry housing needs assessment, effective instant `2026-10-30T16:00:00Z`, mobile writes using a measured 420-minute date correction.
6
+
7
+ ## Mobile writes store yes/no answers empty
8
+
9
+ On a fresh client with no prior answers, `POST /assessmentsmobile/AddAssessmentAnswer` returned success for a `yes_no` question with `answer_value` `"Yes"`, `"1"`/`"0"`, and `"true"`/`"false"`. Each request created a row at the intended effective date, but its value read back empty through the generic assessment read and `assessmentsmobile` answer read. Lookup, text, date, integer, and money answers and a new subassessment row sent through the same endpoint saved normally. An integrator's 30-answer entry batch showed the same empty values.
10
+
11
+ **Ask WellSky:** What `answer_value` does the mobile endpoint accept for `yes_no` questions? Should a write that stores no value return an error?
12
+
13
+ The same loss occurs in new subassessment rows: a Coordinated Entry Event row written through the mobile endpoint kept its dates, event and referral result but lost a yes/no result and a provider-valued location. The standard endpoint saved both.
14
+
15
+ A mobile update to an existing row's `recordset_id` changed only the cells sent and kept the others, including a provider-valued location, but a yes/no cell sent the same way was not stored.
16
+
17
+ **Ask WellSky:** Are provider-valued answers supported by the mobile endpoint, and in what format? Can the standard endpoint update an existing row, so yes/no results recorded after an event can be saved?
18
+
19
+ ## Mobile writes fail for an existing row
20
+
21
+ A second mobile write for a question that already had a row at the same effective date returned HTTP 400 with the body `An error occurred while saving the entity changes. See the inner exception for details.` It happened for a yes/no question and an integer question. No row changed.
22
+
23
+ **Ask WellSky:** Is the mobile endpoint intended to only insert rows? Can the response identify the conflicting question instead of hiding the database error?
24
+
25
+ ## Standard writes require every mandatory answer
26
+
27
+ `POST` to the standard assessment endpoint stored `"Yes"` and `"No"`, rejected `"true"`/`"false"`, updated existing rows at the same date (including empty yes/no rows created by mobile writes), and changed no answers outside the request. A request that omitted any mandatory answer returned `All mandatory answers are not provided`, even when that answer was already recorded at the same date. A conditionally mandatory lookup sent as `""` was accepted and stored empty.
28
+
29
+ **Ask WellSky:** Can an update supply only the changed answers? How should a client mark a conditionally mandatory answer as not applicable?
30
+
31
+ ## Generic reads omit yes/no answers at their effective instant
32
+
33
+ After a successful standard write, the generic assessment read with `dateEffective` equal to the saved instant returned `null` for the yes/no answers, while other types at the same instant read normally. A query one second later, and the mobile read, returned the saved values. The standard create response also returned `null` for them.
34
+
35
+ **Ask WellSky:** Is the effective-date comparison for yes/no answers intended to exclude the queried instant?
@@ -38,4 +38,6 @@ Installed checks also run the client-creation, attachment-retry, session-authent
38
38
 
39
39
  `independent-needs.mts` discovers tenant-wide service types and separate provider quicklists, records explicit PIT needs, and maps a chosen taxonomy type to the service recorder. Dependencies, event keys, provider, and selections are supplied by the application.
40
40
 
41
+ [coordinated-entry-events.mts](coordinated-entry-events.mts) records application-supplied Coordinated Entry Events with durable keys, verified lookup labels, and event-specific follow-up answers such as referral locations and results. It compiles against the installed package without opening a live session.
42
+
41
43
  [current-living-situation.mts](current-living-situation.mts) records application-supplied outreach or night-by-night shelter contact events with distinct durable keys and optional recording-program picklist selections. It compiles and imports against the installed package without opening a live session.
@@ -0,0 +1,37 @@
1
+ import type {WellSkyClient, PicklistSelector} from '@choiceapps/communityservices-sdk';
2
+ import {createCoordinatedEntryEventRecorder, type CoordinatedEntryEventFields, type UiMutationStore} from '@choiceapps/communityservices-sdk/browser';
3
+
4
+ /** Source events from the application. The recorder checks each event's follow-up answers. */
5
+ export async function recordCoordinatedEntryEvents(
6
+ api: WellSkyClient,
7
+ store: UiMutationStore,
8
+ fields: CoordinatedEntryEventFields,
9
+ /** Tenant-verified write labels keyed by field and option code. */
10
+ verifiedLabels: ReadonlyMap<string, string>,
11
+ target: {clientId: number; enrollmentId: number; assessmentCode: string},
12
+ events: readonly {
13
+ eventKey: string; eventDate: string; event: PicklistSelector;
14
+ housedSafely?: boolean; enrolledInAftercare?: boolean;
15
+ locationProviderId?: number; result?: {option: PicklistSelector; date: string};
16
+ }[],
17
+ ) {
18
+ const recorder = createCoordinatedEntryEventRecorder({
19
+ api, store, fields,
20
+ writeLabel: (question, option) => verifiedLabels.get(`${question.fieldName}:${option.code}`),
21
+ });
22
+ const receipts = [];
23
+ for (const event of events) {
24
+ receipts.push(await recorder.record({
25
+ ...target,
26
+ key: event.eventKey,
27
+ eventDate: event.eventDate,
28
+ event: {option: event.event},
29
+ ...(event.housedSafely !== undefined ? {housedSafely: event.housedSafely} : {}),
30
+ ...(event.enrolledInAftercare !== undefined ? {enrolledInAftercare: event.enrolledInAftercare} : {}),
31
+ ...(event.locationProviderId !== undefined ? {location: event.locationProviderId} : {}),
32
+ ...(event.result ? {referralResult: {option: event.result.option}, resultDate: event.result.date} : {}),
33
+ }));
34
+ }
35
+ // Replay the same event keys with the same store to verify without another save.
36
+ return receipts;
37
+ }
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  createEnrollmentWorkflow,
3
+ type EntryExitType,
3
4
  type EnrollmentAssessmentPlan,
4
5
  type EnrollmentWorkflowDiscovery,
5
6
  type EnrollmentWorkflowResult,
@@ -7,13 +8,15 @@ import {
7
8
  } from '@choiceapps/communityservices-sdk/browser';
8
9
 
9
10
  /** Call with a REST client, authenticated discovery/enrollment adapters,
10
- * a birthDates adapter for DOB plans, and the same durable store on every run. This example does not log in or run itself. */
11
+ * a birthDates adapter for DOB plans, and the same durable store on every run.
12
+ * Types other than HUD/VA require dependencies.assessmentLocations from current provider discovery.
13
+ * This example does not log in or run itself. */
11
14
  export async function enrollAndReview(
12
15
  dependencies:EnrollmentWorkflowOptions,
13
16
  input:{
14
17
  clientId:number;
15
18
  providerId:number;
16
- type:'HUD'|'VA';
19
+ type:EntryExitType;
17
20
  /** Stable application record ID, not a new UUID on each retry. */
18
21
  sourceId:string;
19
22
  entryDate:string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@choiceapps/communityservices-sdk",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
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",