@choiceapps/communityservices-sdk 0.5.0 → 0.7.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.
- package/CHANGELOG.md +18 -0
- package/browser/enrollment-workflow.d.mts +54 -4
- package/browser/enrollment-workflow.mjs +89 -33
- package/browser/entry-correction.mjs +53 -10
- package/browser/entry-row-registration.mjs +63 -0
- package/browser/household-enrollment-discovery.mjs +49 -0
- package/browser/index.d.mts +3 -1
- package/browser/playwright-session.mjs +4 -3
- package/browser/workflow-error-codes.mjs +198 -0
- package/dist/cjs/mobile-preflight.d.ts +19 -0
- package/dist/cjs/mobile-preflight.d.ts.map +1 -0
- package/dist/cjs/mobile-preflight.js +75 -0
- package/dist/cjs/mobile-preflight.js.map +1 -0
- package/dist/cjs/v2.d.ts +2 -0
- package/dist/cjs/v2.d.ts.map +1 -1
- package/dist/cjs/v2.js +2 -0
- package/dist/cjs/v2.js.map +1 -1
- package/dist/cjs/v3.d.ts +2 -0
- package/dist/cjs/v3.d.ts.map +1 -1
- package/dist/cjs/v3.js +2 -0
- package/dist/cjs/v3.js.map +1 -1
- package/dist/cjs/write-labels.d.ts +13 -0
- package/dist/cjs/write-labels.d.ts.map +1 -0
- package/dist/cjs/write-labels.js +68 -0
- package/dist/cjs/write-labels.js.map +1 -0
- package/dist/esm/mobile-preflight.d.ts +19 -0
- package/dist/esm/mobile-preflight.d.ts.map +1 -0
- package/dist/esm/mobile-preflight.js +70 -0
- package/dist/esm/mobile-preflight.js.map +1 -0
- package/dist/esm/v2.d.ts +2 -0
- package/dist/esm/v2.d.ts.map +1 -1
- package/dist/esm/v2.js +2 -0
- package/dist/esm/v2.js.map +1 -1
- package/dist/esm/v3.d.ts +2 -0
- package/dist/esm/v3.d.ts.map +1 -1
- package/dist/esm/v3.js +2 -0
- package/dist/esm/v3.js.map +1 -1
- package/dist/esm/write-labels.d.ts +13 -0
- package/dist/esm/write-labels.d.ts.map +1 -0
- package/dist/esm/write-labels.js +65 -0
- package/dist/esm/write-labels.js.map +1 -0
- package/docs/assessments.md +38 -1
- package/docs/browser-session.md +1 -1
- package/docs/capabilities.md +15 -2
- package/docs/enrollments.md +75 -3
- package/docs/reference.md +3 -2
- package/docs/troubleshooting.md +20 -0
- package/examples/README.md +2 -0
- package/examples/checked-assessment-writes.mts +14 -0
- package/examples/enrollment-discovery-and-correction.mts +19 -0
- package/examples/entry-correction-workflow.mts +8 -0
- package/package.json +1 -1
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createReadBackWriteLabelResolver = createReadBackWriteLabelResolver;
|
|
4
|
+
const metadata_js_1 = require("./metadata.js");
|
|
5
|
+
const errors_js_1 = require("./errors.js");
|
|
6
|
+
const validation_js_1 = require("./validation.js");
|
|
7
|
+
/** Builds labels only from exact readback evidence. It never infers HUD suffixes.
|
|
8
|
+
* Unobserved or changed options return undefined, so the standard encoder refuses them. */
|
|
9
|
+
function createReadBackWriteLabelResolver(definition, observations) {
|
|
10
|
+
const questions = new Map();
|
|
11
|
+
for (const { question } of (0, metadata_js_1.flattenQuestions)(definition.questions)) {
|
|
12
|
+
if (question.questionType !== 'lookup')
|
|
13
|
+
continue;
|
|
14
|
+
const prior = questions.get(question.questionId);
|
|
15
|
+
if (prior && JSON.stringify(prior) !== JSON.stringify(question))
|
|
16
|
+
throw new errors_js_1.ValidationError('ambiguous write-label question');
|
|
17
|
+
questions.set(question.questionId, structuredClone(question));
|
|
18
|
+
}
|
|
19
|
+
const labels = new Map();
|
|
20
|
+
const identity = (questionId, optionId) => JSON.stringify([questionId, optionId]);
|
|
21
|
+
const visit = (value, depth = 0) => {
|
|
22
|
+
if (depth > 40)
|
|
23
|
+
throw new errors_js_1.ValidationError('write-label observation exceeds nesting limit');
|
|
24
|
+
if (Array.isArray(value)) {
|
|
25
|
+
for (const child of value)
|
|
26
|
+
visit(child, depth + 1);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
if (!(0, validation_js_1.isRecord)(value))
|
|
30
|
+
return;
|
|
31
|
+
if (value.question_type === 'lookup' && typeof value.question_id === 'number') {
|
|
32
|
+
const question = questions.get(value.question_id);
|
|
33
|
+
if (question) {
|
|
34
|
+
if (value.virt_field_name !== question.fieldName)
|
|
35
|
+
throw new errors_js_1.ValidationError('write-label question identity mismatch');
|
|
36
|
+
const raw = value.answer_value;
|
|
37
|
+
const id = typeof raw === 'number' ? raw : typeof raw === 'string' && /^\d+$/.test(raw) ? Number(raw) : null;
|
|
38
|
+
if (id !== null && Number.isSafeInteger(id) && typeof value.answer_value_text === 'string' && value.answer_value_text.trim()) {
|
|
39
|
+
const options = question.options.filter(option => option.id === id);
|
|
40
|
+
if (options.length !== 1)
|
|
41
|
+
throw new errors_js_1.ValidationError('write-label option identity mismatch', [question.fieldName ?? String(question.questionId)]);
|
|
42
|
+
const key = identity(question.questionId, id), prior = labels.get(key);
|
|
43
|
+
if (prior !== undefined && prior !== value.answer_value_text)
|
|
44
|
+
throw new errors_js_1.ValidationError('conflicting write-label observations', [question.fieldName ?? String(question.questionId)]);
|
|
45
|
+
labels.set(key, value.answer_value_text);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
// Traverse known response/assessment containers only. Do not interpret arbitrary strings as data.
|
|
50
|
+
for (const key of ['value', 'values', 'sub_assessment_question_list', 'sub_assessment_answer_list', 'sub_assessment_list'])
|
|
51
|
+
if (value[key] !== undefined)
|
|
52
|
+
visit(value[key], depth + 1);
|
|
53
|
+
};
|
|
54
|
+
for (const observation of observations) {
|
|
55
|
+
if (observation.tenant !== definition.tenant)
|
|
56
|
+
throw new errors_js_1.ValidationError('write-label observation tenant mismatch');
|
|
57
|
+
visit(observation.response);
|
|
58
|
+
}
|
|
59
|
+
const sameOption = (a, b) => a.id === b.id && a.picklistId === b.picklistId && a.code === b.code && a.picklistCode === b.picklistCode && a.name === b.name && a.hudVisibility === b.hudVisibility;
|
|
60
|
+
const resolver = (question, option) => {
|
|
61
|
+
const known = questions.get(question.questionId);
|
|
62
|
+
if (!known || known.fieldName !== question.fieldName || question.questionType !== 'lookup' || !known.options.some(o => sameOption(o, option)))
|
|
63
|
+
return undefined;
|
|
64
|
+
return labels.get(identity(question.questionId, option.id));
|
|
65
|
+
};
|
|
66
|
+
return Object.freeze(Object.assign(resolver, { tenant: definition.tenant }));
|
|
67
|
+
}
|
|
68
|
+
//# sourceMappingURL=write-labels.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"write-labels.js","sourceRoot":"","sources":["../../src/write-labels.ts"],"names":[],"mappings":";;AAWA,4EA8CC;AAxDD,+CAA+C;AAE/C,2CAA4C;AAC5C,mDAAyC;AAKzC;2FAC2F;AAC3F,SAAgB,gCAAgC,CAC/C,UAA+B,EAC/B,YAA6C;IAE7C,MAAM,SAAS,GAAC,IAAI,GAAG,EAA6B,CAAC;IACrD,KAAI,MAAM,EAAC,QAAQ,EAAC,IAAI,IAAA,8BAAgB,EAAC,UAAU,CAAC,SAAS,CAAC,EAAC,CAAC;QAC/D,IAAG,QAAQ,CAAC,YAAY,KAAG,QAAQ;YAAC,SAAS;QAC7C,MAAM,KAAK,GAAC,SAAS,CAAC,GAAG,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC;QAC/C,IAAG,KAAK,IAAE,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,KAAG,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC;YAAC,MAAM,IAAI,2BAAe,CAAC,gCAAgC,CAAC,CAAC;QACvH,SAAS,CAAC,GAAG,CAAC,QAAQ,CAAC,UAAU,EAAC,eAAe,CAAC,QAAQ,CAAC,CAAC,CAAC;IAC9D,CAAC;IACD,MAAM,MAAM,GAAC,IAAI,GAAG,EAAiB,CAAC;IACtC,MAAM,QAAQ,GAAC,CAAC,UAAiB,EAAC,QAAe,EAAC,EAAE,CAAA,IAAI,CAAC,SAAS,CAAC,CAAC,UAAU,EAAC,QAAQ,CAAC,CAAC,CAAC;IAC1F,MAAM,KAAK,GAAC,CAAC,KAAa,EAAC,KAAK,GAAC,CAAC,EAAM,EAAE;QACzC,IAAG,KAAK,GAAC,EAAE;YAAC,MAAM,IAAI,2BAAe,CAAC,+CAA+C,CAAC,CAAC;QACvF,IAAG,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAC,CAAC;YAAA,KAAI,MAAM,KAAK,IAAI,KAAK;gBAAC,KAAK,CAAC,KAAK,EAAC,KAAK,GAAC,CAAC,CAAC,CAAC;YAAA,OAAO;QAAA,CAAC;QAC/E,IAAG,CAAC,IAAA,wBAAQ,EAAC,KAAK,CAAC;YAAC,OAAO;QAC3B,IAAG,KAAK,CAAC,aAAa,KAAG,QAAQ,IAAE,OAAO,KAAK,CAAC,WAAW,KAAG,QAAQ,EAAC,CAAC;YACvE,MAAM,QAAQ,GAAC,SAAS,CAAC,GAAG,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC;YAChD,IAAG,QAAQ,EAAC,CAAC;gBACZ,IAAG,KAAK,CAAC,eAAe,KAAG,QAAQ,CAAC,SAAS;oBAAC,MAAM,IAAI,2BAAe,CAAC,wCAAwC,CAAC,CAAC;gBAClH,MAAM,GAAG,GAAC,KAAK,CAAC,YAAY,CAAC;gBAC7B,MAAM,EAAE,GAAC,OAAO,GAAG,KAAG,QAAQ,CAAA,CAAC,CAAA,GAAG,CAAA,CAAC,CAAA,OAAO,GAAG,KAAG,QAAQ,IAAE,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA,CAAC,CAAA,MAAM,CAAC,GAAG,CAAC,CAAA,CAAC,CAAA,IAAI,CAAC;gBAC7F,IAAG,EAAE,KAAG,IAAI,IAAE,MAAM,CAAC,aAAa,CAAC,EAAE,CAAC,IAAE,OAAO,KAAK,CAAC,iBAAiB,KAAG,QAAQ,IAAE,KAAK,CAAC,iBAAiB,CAAC,IAAI,EAAE,EAAC,CAAC;oBAClH,MAAM,OAAO,GAAC,QAAQ,CAAC,OAAO,CAAC,MAAM,CAAC,MAAM,CAAA,EAAE,CAAA,MAAM,CAAC,EAAE,KAAG,EAAE,CAAC,CAAC;oBAC9D,IAAG,OAAO,CAAC,MAAM,KAAG,CAAC;wBAAC,MAAM,IAAI,2BAAe,CAAC,sCAAsC,EAAC,CAAC,QAAQ,CAAC,SAAS,IAAE,MAAM,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;oBAC1I,MAAM,GAAG,GAAC,QAAQ,CAAC,QAAQ,CAAC,UAAU,EAAC,EAAE,CAAC,EAAC,KAAK,GAAC,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;oBACjE,IAAG,KAAK,KAAG,SAAS,IAAE,KAAK,KAAG,KAAK,CAAC,iBAAiB;wBAAC,MAAM,IAAI,2BAAe,CAAC,sCAAsC,EAAC,CAAC,QAAQ,CAAC,SAAS,IAAE,MAAM,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;oBAC1K,MAAM,CAAC,GAAG,CAAC,GAAG,EAAC,KAAK,CAAC,iBAAiB,CAAC,CAAC;gBACzC,CAAC;YACF,CAAC;QACF,CAAC;QACD,kGAAkG;QAClG,KAAI,MAAM,GAAG,IAAI,CAAC,OAAO,EAAC,QAAQ,EAAC,8BAA8B,EAAC,4BAA4B,EAAC,qBAAqB,CAAC;YAAC,IAAG,KAAK,CAAC,GAAG,CAAC,KAAG,SAAS;gBAAC,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,EAAC,KAAK,GAAC,CAAC,CAAC,CAAC;IAC3K,CAAC,CAAC;IACF,KAAI,MAAM,WAAW,IAAI,YAAY,EAAC,CAAC;QACtC,IAAG,WAAW,CAAC,MAAM,KAAG,UAAU,CAAC,MAAM;YAAC,MAAM,IAAI,2BAAe,CAAC,yCAAyC,CAAC,CAAC;QAC/G,KAAK,CAAC,WAAW,CAAC,QAAQ,CAAC,CAAC;IAC7B,CAAC;IACD,MAAM,UAAU,GAAC,CAAC,CAAgB,EAAC,CAAgB,EAAC,EAAE,CAAA,CAAC,CAAC,EAAE,KAAG,CAAC,CAAC,EAAE,IAAE,CAAC,CAAC,UAAU,KAAG,CAAC,CAAC,UAAU,IAAE,CAAC,CAAC,IAAI,KAAG,CAAC,CAAC,IAAI,IAAE,CAAC,CAAC,YAAY,KAAG,CAAC,CAAC,YAAY,IAAE,CAAC,CAAC,IAAI,KAAG,CAAC,CAAC,IAAI,IAAE,CAAC,CAAC,aAAa,KAAG,CAAC,CAAC,aAAa,CAAC;IACrM,MAAM,QAAQ,GAAC,CAAC,QAA2B,EAAC,MAAqB,EAAC,EAAE;QACnE,MAAM,KAAK,GAAC,SAAS,CAAC,GAAG,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC;QAC/C,IAAG,CAAC,KAAK,IAAE,KAAK,CAAC,SAAS,KAAG,QAAQ,CAAC,SAAS,IAAE,QAAQ,CAAC,YAAY,KAAG,QAAQ,IAAE,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAA,EAAE,CAAA,UAAU,CAAC,CAAC,EAAC,MAAM,CAAC,CAAC;YAAC,OAAO,SAAS,CAAC;QACjJ,OAAO,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC,QAAQ,CAAC,UAAU,EAAC,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC;IAC5D,CAAC,CAAC;IACF,OAAO,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,QAAQ,EAAC,EAAC,MAAM,EAAC,UAAU,CAAC,MAAM,EAAC,CAAC,CAAC,CAAC;AAC1E,CAAC","sourcesContent":["import type {AssessmentDefinition,PicklistOption,QuestionDefinition} from './metadata.js';\nimport {flattenQuestions} from './metadata.js';\nimport type {EncodingOptions} from './encoding.js';\nimport {ValidationError} from './errors.js';\nimport {isRecord} from './validation.js';\n\nexport interface WriteLabelObservation {tenant:string;response:unknown;}\nexport type ReadBackWriteLabelResolver = NonNullable<EncodingOptions['writeLabel']> & {readonly tenant:string};\n\n/** Builds labels only from exact readback evidence. It never infers HUD suffixes.\n * Unobserved or changed options return undefined, so the standard encoder refuses them. */\nexport function createReadBackWriteLabelResolver(\n definition:AssessmentDefinition,\n observations:readonly WriteLabelObservation[],\n):ReadBackWriteLabelResolver{\n const questions=new Map<number,QuestionDefinition>();\n for(const {question} of flattenQuestions(definition.questions)){\n if(question.questionType!=='lookup')continue;\n const prior=questions.get(question.questionId);\n if(prior&&JSON.stringify(prior)!==JSON.stringify(question))throw new ValidationError('ambiguous write-label question');\n questions.set(question.questionId,structuredClone(question));\n }\n const labels=new Map<string,string>();\n const identity=(questionId:number,optionId:number)=>JSON.stringify([questionId,optionId]);\n const visit=(value:unknown,depth=0):void=>{\n if(depth>40)throw new ValidationError('write-label observation exceeds nesting limit');\n if(Array.isArray(value)){for(const child of value)visit(child,depth+1);return;}\n if(!isRecord(value))return;\n if(value.question_type==='lookup'&&typeof value.question_id==='number'){\n const question=questions.get(value.question_id);\n if(question){\n if(value.virt_field_name!==question.fieldName)throw new ValidationError('write-label question identity mismatch');\n const raw=value.answer_value;\n const id=typeof raw==='number'?raw:typeof raw==='string'&&/^\\d+$/.test(raw)?Number(raw):null;\n if(id!==null&&Number.isSafeInteger(id)&&typeof value.answer_value_text==='string'&&value.answer_value_text.trim()){\n const options=question.options.filter(option=>option.id===id);\n if(options.length!==1)throw new ValidationError('write-label option identity mismatch',[question.fieldName??String(question.questionId)]);\n const key=identity(question.questionId,id),prior=labels.get(key);\n if(prior!==undefined&&prior!==value.answer_value_text)throw new ValidationError('conflicting write-label observations',[question.fieldName??String(question.questionId)]);\n labels.set(key,value.answer_value_text);\n }\n }\n }\n // Traverse known response/assessment containers only. Do not interpret arbitrary strings as data.\n for(const key of ['value','values','sub_assessment_question_list','sub_assessment_answer_list','sub_assessment_list'])if(value[key]!==undefined)visit(value[key],depth+1);\n };\n for(const observation of observations){\n if(observation.tenant!==definition.tenant)throw new ValidationError('write-label observation tenant mismatch');\n visit(observation.response);\n }\n const sameOption=(a:PicklistOption,b:PicklistOption)=>a.id===b.id&&a.picklistId===b.picklistId&&a.code===b.code&&a.picklistCode===b.picklistCode&&a.name===b.name&&a.hudVisibility===b.hudVisibility;\n const resolver=(question:QuestionDefinition,option:PicklistOption)=>{\n const known=questions.get(question.questionId);\n if(!known||known.fieldName!==question.fieldName||question.questionType!=='lookup'||!known.options.some(o=>sameOption(o,option)))return undefined;\n return labels.get(identity(question.questionId,option.id));\n };\n return Object.freeze(Object.assign(resolver,{tenant:definition.tenant}));\n}\n"]}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { WellSkyClient } from './generated/v3.js';
|
|
2
|
+
import { ValidationError } from './errors.js';
|
|
3
|
+
type RawMobileWrite = Parameters<WellSkyClient['assessmentsMobile']['addAnswer']>[0];
|
|
4
|
+
export type CheckedMobileWriteOptions = Omit<RawMobileWrite, 'query'> & {
|
|
5
|
+
query: {
|
|
6
|
+
dateEffective: string;
|
|
7
|
+
};
|
|
8
|
+
};
|
|
9
|
+
export type MobilePreflightCode = 'MOBILE_PREFLIGHT_INPUT_INVALID' | 'MOBILE_PREFLIGHT_READ_FAILED' | 'MOBILE_PREFLIGHT_UNAVAILABLE' | 'MOBILE_ANSWER_EXISTS' | 'MOBILE_PREFLIGHT_CANCELLED';
|
|
10
|
+
export declare class MobileAnswerPreflightError extends ValidationError {
|
|
11
|
+
readonly code: MobilePreflightCode;
|
|
12
|
+
readonly committed = false;
|
|
13
|
+
constructor(code: MobilePreflightCode, fields?: readonly string[], cause?: unknown);
|
|
14
|
+
}
|
|
15
|
+
/** Read-before-write protection for scalar inserts, including questions shared by instruments.
|
|
16
|
+
* Does not journal, retry writes, or make the read/write pair atomic. */
|
|
17
|
+
export declare function writeMobileAnswersChecked(api: Pick<WellSkyClient, 'assessments' | 'assessmentsMobile'>, options: CheckedMobileWriteOptions): ReturnType<WellSkyClient['assessmentsMobile']['addAnswer']>;
|
|
18
|
+
export {};
|
|
19
|
+
//# sourceMappingURL=mobile-preflight.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mobile-preflight.d.ts","sourceRoot":"","sources":["../../src/mobile-preflight.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,aAAa,EAAY,MAAM,mBAAmB,CAAC;AAChE,OAAO,EAAC,eAAe,EAAC,MAAM,aAAa,CAAC;AAK5C,KAAK,cAAc,GAAG,UAAU,CAAC,aAAa,CAAC,mBAAmB,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AACrF,MAAM,MAAM,yBAAyB,GAAG,IAAI,CAAC,cAAc,EAAC,OAAO,CAAC,GAAG;IAAC,KAAK,EAAC;QAAC,aAAa,EAAC,MAAM,CAAA;KAAC,CAAA;CAAC,CAAC;AACtG,MAAM,MAAM,mBAAmB,GAAG,gCAAgC,GAAC,8BAA8B,GAAC,8BAA8B,GAAC,sBAAsB,GAAC,4BAA4B,CAAC;AACrL,qBAAa,0BAA2B,SAAQ,eAAe;IAElD,QAAQ,CAAC,IAAI,EAAC,mBAAmB;IAD7C,QAAQ,CAAC,SAAS,SAAO;gBACJ,IAAI,EAAC,mBAAmB,EAAC,MAAM,GAAC,SAAS,MAAM,EAAK,EAAC,KAAK,CAAC,EAAC,OAAO;CAIxF;AAED;yEACyE;AACzE,wBAAsB,yBAAyB,CAC9C,GAAG,EAAC,IAAI,CAAC,aAAa,EAAC,aAAa,GAAC,mBAAmB,CAAC,EACzD,OAAO,EAAC,yBAAyB,GAChC,UAAU,CAAC,aAAa,CAAC,mBAAmB,CAAC,CAAC,WAAW,CAAC,CAAC,CAkC5D"}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { ValidationError } from './errors.js';
|
|
2
|
+
import { parseRestInstant } from './encoding.js';
|
|
3
|
+
import { toItems } from './runtime.js';
|
|
4
|
+
import { isRecord } from './validation.js';
|
|
5
|
+
export class MobileAnswerPreflightError extends ValidationError {
|
|
6
|
+
code;
|
|
7
|
+
committed = false;
|
|
8
|
+
constructor(code, fields = [], cause) {
|
|
9
|
+
super('mobile assessment preflight', fields);
|
|
10
|
+
this.code = code;
|
|
11
|
+
this.name = 'MobileAnswerPreflightError';
|
|
12
|
+
if (cause !== undefined)
|
|
13
|
+
Object.defineProperty(this, 'cause', { value: cause, enumerable: false });
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
/** Read-before-write protection for scalar inserts, including questions shared by instruments.
|
|
17
|
+
* Does not journal, retry writes, or make the read/write pair atomic. */
|
|
18
|
+
export async function writeMobileAnswersChecked(api, options) {
|
|
19
|
+
function fail(code, fields = [], cause) { throw new MobileAnswerPreflightError(code, fields, cause); }
|
|
20
|
+
const checkSignal = () => { if (options.signal?.aborted)
|
|
21
|
+
fail('MOBILE_PREFLIGHT_CANCELLED', [], options.signal.reason); };
|
|
22
|
+
if (!options?.path || !Number.isSafeInteger(options.path.clientId) || options.path.clientId <= 0 || typeof options.path.assessmentCode !== 'string' || !options.path.assessmentCode.trim() || !Array.isArray(options.body) || !options.body.length || typeof options.query?.dateEffective !== 'string')
|
|
23
|
+
fail('MOBILE_PREFLIGHT_INPUT_INVALID');
|
|
24
|
+
const instant = options.query.dateEffective;
|
|
25
|
+
if (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.000)?(?:Z|[+-]\d{2}:\d{2})$/.test(instant) || !Number.isFinite(parseRestInstant(instant)) || new Date(instant.slice(0, 19) + 'Z').toISOString().slice(0, 19) !== instant.slice(0, 19))
|
|
26
|
+
fail('MOBILE_PREFLIGHT_INPUT_INVALID', ['dateEffective']);
|
|
27
|
+
checkSignal();
|
|
28
|
+
const captured = { ...options, path: { ...options.path }, query: { ...options.query }, body: structuredClone(options.body) };
|
|
29
|
+
const scalars = [], seen = new Set(), ids = new Set();
|
|
30
|
+
for (const q of captured.body) {
|
|
31
|
+
if (!isRecord(q) || typeof q.question_type !== 'string' || typeof q.virt_field_name !== 'string' || !q.virt_field_name || typeof q.question_id !== 'number' || !Number.isSafeInteger(q.question_id) || q.question_id <= 0 || seen.has(q.virt_field_name) || ids.has(q.question_id))
|
|
32
|
+
fail('MOBILE_PREFLIGHT_INPUT_INVALID');
|
|
33
|
+
seen.add(q.virt_field_name);
|
|
34
|
+
ids.add(q.question_id);
|
|
35
|
+
if (q.question_type !== 'sub_assessment')
|
|
36
|
+
scalars.push({ id: q.question_id, field: q.virt_field_name, type: q.question_type });
|
|
37
|
+
}
|
|
38
|
+
if (scalars.length) {
|
|
39
|
+
let response;
|
|
40
|
+
try {
|
|
41
|
+
response = await api.assessments.get({ path: captured.path, query: { dateEffective: new Date(parseRestInstant(instant) + 1000).toISOString() }, ...(options.signal ? { signal: options.signal } : {}), ...(options.timeoutMs === undefined ? {} : { timeoutMs: options.timeoutMs }), ...(options.correlationId === undefined ? {} : { correlationId: options.correlationId }) });
|
|
42
|
+
}
|
|
43
|
+
catch (cause) {
|
|
44
|
+
fail('MOBILE_PREFLIGHT_READ_FAILED', scalars.map(q => q.field), cause);
|
|
45
|
+
}
|
|
46
|
+
const rows = toItems(response), conflicts = [];
|
|
47
|
+
for (const q of scalars) {
|
|
48
|
+
const found = rows.filter(r => r.virt_field_name === q.field);
|
|
49
|
+
if (found.length !== 1 || found[0]?.question_id !== q.id || found[0]?.question_type !== q.type)
|
|
50
|
+
fail('MOBILE_PREFLIGHT_UNAVAILABLE', [q.field]);
|
|
51
|
+
const row = found[0];
|
|
52
|
+
if (!row)
|
|
53
|
+
fail('MOBILE_PREFLIGHT_UNAVAILABLE', [q.field]);
|
|
54
|
+
if (row.date_effective !== null && row.date_effective !== undefined && row.date_effective !== '') {
|
|
55
|
+
const recorded = parseRestInstant(row.date_effective);
|
|
56
|
+
if (!Number.isFinite(recorded) || recorded > parseRestInstant(instant))
|
|
57
|
+
fail('MOBILE_PREFLIGHT_UNAVAILABLE', [q.field]);
|
|
58
|
+
if (recorded === parseRestInstant(instant))
|
|
59
|
+
conflicts.push(q.field);
|
|
60
|
+
}
|
|
61
|
+
else if (row.answer_value !== null && row.answer_value !== undefined && row.answer_value !== '')
|
|
62
|
+
fail('MOBILE_PREFLIGHT_UNAVAILABLE', [q.field]);
|
|
63
|
+
}
|
|
64
|
+
if (conflicts.length)
|
|
65
|
+
fail('MOBILE_ANSWER_EXISTS', conflicts);
|
|
66
|
+
}
|
|
67
|
+
checkSignal();
|
|
68
|
+
return api.assessmentsMobile.addAnswer(captured);
|
|
69
|
+
}
|
|
70
|
+
//# sourceMappingURL=mobile-preflight.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mobile-preflight.js","sourceRoot":"","sources":["../../src/mobile-preflight.ts"],"names":[],"mappings":"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","sourcesContent":["import type {WellSkyClient,Operations} from './generated/v3.js';\nimport {ValidationError} from './errors.js';\nimport {parseRestInstant} from './encoding.js';\nimport {toItems} from './runtime.js';\nimport {isRecord} from './validation.js';\n\ntype RawMobileWrite = Parameters<WellSkyClient['assessmentsMobile']['addAnswer']>[0];\nexport type CheckedMobileWriteOptions = Omit<RawMobileWrite,'query'> & {query:{dateEffective:string}};\nexport type MobilePreflightCode = 'MOBILE_PREFLIGHT_INPUT_INVALID'|'MOBILE_PREFLIGHT_READ_FAILED'|'MOBILE_PREFLIGHT_UNAVAILABLE'|'MOBILE_ANSWER_EXISTS'|'MOBILE_PREFLIGHT_CANCELLED';\nexport class MobileAnswerPreflightError extends ValidationError {\n readonly committed=false;\n constructor(readonly code:MobilePreflightCode,fields:readonly string[]=[],cause?:unknown){\n super('mobile assessment preflight',fields);this.name='MobileAnswerPreflightError';\n if(cause!==undefined)Object.defineProperty(this,'cause',{value:cause,enumerable:false});\n }\n}\n\n/** Read-before-write protection for scalar inserts, including questions shared by instruments.\n * Does not journal, retry writes, or make the read/write pair atomic. */\nexport async function writeMobileAnswersChecked(\n api:Pick<WellSkyClient,'assessments'|'assessmentsMobile'>,\n options:CheckedMobileWriteOptions,\n):ReturnType<WellSkyClient['assessmentsMobile']['addAnswer']>{\n function fail(code:MobilePreflightCode,fields:readonly string[]=[],cause?:unknown):never{throw new MobileAnswerPreflightError(code,fields,cause);}\n const checkSignal=()=>{if(options.signal?.aborted)fail('MOBILE_PREFLIGHT_CANCELLED',[],options.signal.reason);};\n if(!options?.path||!Number.isSafeInteger(options.path.clientId)||options.path.clientId<=0||typeof options.path.assessmentCode!=='string'||!options.path.assessmentCode.trim()||!Array.isArray(options.body)||!options.body.length||typeof options.query?.dateEffective!=='string')fail('MOBILE_PREFLIGHT_INPUT_INVALID');\n const instant=options.query.dateEffective;\n if(!/^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:\\.000)?(?:Z|[+-]\\d{2}:\\d{2})$/.test(instant)||!Number.isFinite(parseRestInstant(instant))||new Date(instant.slice(0,19)+'Z').toISOString().slice(0,19)!==instant.slice(0,19))fail('MOBILE_PREFLIGHT_INPUT_INVALID',['dateEffective']);\n checkSignal();\n const captured={...options,path:{...options.path},query:{...options.query},body:structuredClone(options.body)};\n const scalars:Array<{id:number;field:string;type:string}>=[],seen=new Set<string>(),ids=new Set<number>();\n for(const q of captured.body){\n if(!isRecord(q)||typeof q.question_type!=='string'||typeof q.virt_field_name!=='string'||!q.virt_field_name||typeof q.question_id!=='number'||!Number.isSafeInteger(q.question_id)||q.question_id<=0||seen.has(q.virt_field_name)||ids.has(q.question_id))fail('MOBILE_PREFLIGHT_INPUT_INVALID');\n seen.add(q.virt_field_name);ids.add(q.question_id);\n if(q.question_type!=='sub_assessment')scalars.push({id:q.question_id,field:q.virt_field_name,type:q.question_type});\n }\n if(scalars.length){\n let response:Awaited<ReturnType<WellSkyClient['assessments']['get']>>;\n try{response=await api.assessments.get({path:captured.path,query:{dateEffective:new Date(parseRestInstant(instant)+1000).toISOString()},...(options.signal?{signal:options.signal}:{}),...(options.timeoutMs===undefined?{}:{timeoutMs:options.timeoutMs}),...(options.correlationId===undefined?{}:{correlationId:options.correlationId})});}\n catch(cause){fail('MOBILE_PREFLIGHT_READ_FAILED',scalars.map(q=>q.field),cause);}\n const rows=toItems<Operations['GET /assessments/{clientId}/{assessmentCode}']['item']>(response),conflicts:string[]=[];\n for(const q of scalars){\n const found=rows.filter(r=>r.virt_field_name===q.field);\n if(found.length!==1||found[0]?.question_id!==q.id||found[0]?.question_type!==q.type)fail('MOBILE_PREFLIGHT_UNAVAILABLE',[q.field]);\n const row=found[0];\n if(!row)fail('MOBILE_PREFLIGHT_UNAVAILABLE',[q.field]);\n if(row.date_effective!==null&&row.date_effective!==undefined&&row.date_effective!==''){\n const recorded=parseRestInstant(row.date_effective);\n if(!Number.isFinite(recorded)||recorded>parseRestInstant(instant))fail('MOBILE_PREFLIGHT_UNAVAILABLE',[q.field]);\n if(recorded===parseRestInstant(instant))conflicts.push(q.field);\n }else if(row.answer_value!==null&&row.answer_value!==undefined&&row.answer_value!=='')fail('MOBILE_PREFLIGHT_UNAVAILABLE',[q.field]);\n }\n if(conflicts.length)fail('MOBILE_ANSWER_EXISTS',conflicts);\n }\n checkSignal();\n return api.assessmentsMobile.addAnswer(captured);\n}\n"]}
|
package/dist/esm/v2.d.ts
CHANGED
|
@@ -10,4 +10,6 @@ export * from "./subassessments.js";
|
|
|
10
10
|
export * from "./enrollment-verifications.js";
|
|
11
11
|
export * from "./gwt-enrollments.js";
|
|
12
12
|
export * from "./program-discovery.js";
|
|
13
|
+
export * from "./mobile-preflight.js";
|
|
14
|
+
export * from "./write-labels.js";
|
|
13
15
|
//# sourceMappingURL=v2.d.ts.map
|
package/dist/esm/v2.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"v2.d.ts","sourceRoot":"","sources":["../../src/v2.ts"],"names":[],"mappings":"AAAA,cAAc,mBAAmB,CAAC;AAClC,cAAc,0BAA0B,CAAC;AACzC,cAAc,cAAc,CAAC;AAC7B,cAAc,eAAe,CAAC;AAC9B,cAAc,aAAa,CAAC;AAC5B,cAAc,eAAe,CAAC;AAC9B,cAAc,mBAAmB,CAAC;AAClC,cAAc,WAAW,CAAC;AAC1B,cAAc,qBAAqB,CAAC;AACpC,cAAc,+BAA+B,CAAC;AAE9C,cAAc,sBAAsB,CAAC;AACrC,cAAc,wBAAwB,CAAC"}
|
|
1
|
+
{"version":3,"file":"v2.d.ts","sourceRoot":"","sources":["../../src/v2.ts"],"names":[],"mappings":"AAAA,cAAc,mBAAmB,CAAC;AAClC,cAAc,0BAA0B,CAAC;AACzC,cAAc,cAAc,CAAC;AAC7B,cAAc,eAAe,CAAC;AAC9B,cAAc,aAAa,CAAC;AAC5B,cAAc,eAAe,CAAC;AAC9B,cAAc,mBAAmB,CAAC;AAClC,cAAc,WAAW,CAAC;AAC1B,cAAc,qBAAqB,CAAC;AACpC,cAAc,+BAA+B,CAAC;AAE9C,cAAc,sBAAsB,CAAC;AACrC,cAAc,wBAAwB,CAAC;AACvC,cAAc,uBAAuB,CAAC;AACtC,cAAc,mBAAmB,CAAC"}
|
package/dist/esm/v2.js
CHANGED
|
@@ -10,4 +10,6 @@ export * from "./subassessments.js";
|
|
|
10
10
|
export * from "./enrollment-verifications.js";
|
|
11
11
|
export * from "./gwt-enrollments.js";
|
|
12
12
|
export * from "./program-discovery.js";
|
|
13
|
+
export * from "./mobile-preflight.js";
|
|
14
|
+
export * from "./write-labels.js";
|
|
13
15
|
//# sourceMappingURL=v2.js.map
|
package/dist/esm/v2.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"v2.js","sourceRoot":"","sources":["../../src/v2.ts"],"names":[],"mappings":"AAAA,cAAc,mBAAmB,CAAC;AAClC,cAAc,0BAA0B,CAAC;AACzC,cAAc,cAAc,CAAC;AAC7B,cAAc,eAAe,CAAC;AAC9B,cAAc,aAAa,CAAC;AAC5B,cAAc,eAAe,CAAC;AAC9B,cAAc,mBAAmB,CAAC;AAClC,cAAc,WAAW,CAAC;AAC1B,cAAc,qBAAqB,CAAC;AACpC,cAAc,+BAA+B,CAAC;AAE9C,cAAc,sBAAsB,CAAC;AACrC,cAAc,wBAAwB,CAAC","sourcesContent":["export * from \"./generated/v2.js\";\nexport * from \"./generated/v2-models.js\";\nexport * from \"./runtime.js\";\nexport * from \"./metadata.js\";\nexport * from \"./errors.js\";\nexport * from \"./encoding.js\";\nexport * from \"./verification.js\";\nexport * from \"./bulk.js\";\nexport * from \"./subassessments.js\";\nexport * from \"./enrollment-verifications.js\";\n\nexport * from \"./gwt-enrollments.js\";\nexport * from \"./program-discovery.js\";\n"]}
|
|
1
|
+
{"version":3,"file":"v2.js","sourceRoot":"","sources":["../../src/v2.ts"],"names":[],"mappings":"AAAA,cAAc,mBAAmB,CAAC;AAClC,cAAc,0BAA0B,CAAC;AACzC,cAAc,cAAc,CAAC;AAC7B,cAAc,eAAe,CAAC;AAC9B,cAAc,aAAa,CAAC;AAC5B,cAAc,eAAe,CAAC;AAC9B,cAAc,mBAAmB,CAAC;AAClC,cAAc,WAAW,CAAC;AAC1B,cAAc,qBAAqB,CAAC;AACpC,cAAc,+BAA+B,CAAC;AAE9C,cAAc,sBAAsB,CAAC;AACrC,cAAc,wBAAwB,CAAC;AACvC,cAAc,uBAAuB,CAAC;AACtC,cAAc,mBAAmB,CAAC","sourcesContent":["export * from \"./generated/v2.js\";\nexport * from \"./generated/v2-models.js\";\nexport * from \"./runtime.js\";\nexport * from \"./metadata.js\";\nexport * from \"./errors.js\";\nexport * from \"./encoding.js\";\nexport * from \"./verification.js\";\nexport * from \"./bulk.js\";\nexport * from \"./subassessments.js\";\nexport * from \"./enrollment-verifications.js\";\n\nexport * from \"./gwt-enrollments.js\";\nexport * from \"./program-discovery.js\";\nexport * from \"./mobile-preflight.js\";\nexport * from \"./write-labels.js\";\n"]}
|
package/dist/esm/v3.d.ts
CHANGED
|
@@ -10,4 +10,6 @@ export * from "./subassessments.js";
|
|
|
10
10
|
export * from "./enrollment-verifications.js";
|
|
11
11
|
export * from "./gwt-enrollments.js";
|
|
12
12
|
export * from "./program-discovery.js";
|
|
13
|
+
export * from "./mobile-preflight.js";
|
|
14
|
+
export * from "./write-labels.js";
|
|
13
15
|
//# sourceMappingURL=v3.d.ts.map
|
package/dist/esm/v3.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"v3.d.ts","sourceRoot":"","sources":["../../src/v3.ts"],"names":[],"mappings":"AAAA,cAAc,mBAAmB,CAAC;AAClC,cAAc,0BAA0B,CAAC;AACzC,cAAc,cAAc,CAAC;AAC7B,cAAc,eAAe,CAAC;AAC9B,cAAc,aAAa,CAAC;AAC5B,cAAc,eAAe,CAAC;AAC9B,cAAc,mBAAmB,CAAC;AAClC,cAAc,WAAW,CAAC;AAC1B,cAAc,qBAAqB,CAAC;AACpC,cAAc,+BAA+B,CAAC;AAE9C,cAAc,sBAAsB,CAAC;AACrC,cAAc,wBAAwB,CAAC"}
|
|
1
|
+
{"version":3,"file":"v3.d.ts","sourceRoot":"","sources":["../../src/v3.ts"],"names":[],"mappings":"AAAA,cAAc,mBAAmB,CAAC;AAClC,cAAc,0BAA0B,CAAC;AACzC,cAAc,cAAc,CAAC;AAC7B,cAAc,eAAe,CAAC;AAC9B,cAAc,aAAa,CAAC;AAC5B,cAAc,eAAe,CAAC;AAC9B,cAAc,mBAAmB,CAAC;AAClC,cAAc,WAAW,CAAC;AAC1B,cAAc,qBAAqB,CAAC;AACpC,cAAc,+BAA+B,CAAC;AAE9C,cAAc,sBAAsB,CAAC;AACrC,cAAc,wBAAwB,CAAC;AACvC,cAAc,uBAAuB,CAAC;AACtC,cAAc,mBAAmB,CAAC"}
|
package/dist/esm/v3.js
CHANGED
|
@@ -10,4 +10,6 @@ export * from "./subassessments.js";
|
|
|
10
10
|
export * from "./enrollment-verifications.js";
|
|
11
11
|
export * from "./gwt-enrollments.js";
|
|
12
12
|
export * from "./program-discovery.js";
|
|
13
|
+
export * from "./mobile-preflight.js";
|
|
14
|
+
export * from "./write-labels.js";
|
|
13
15
|
//# sourceMappingURL=v3.js.map
|
package/dist/esm/v3.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"v3.js","sourceRoot":"","sources":["../../src/v3.ts"],"names":[],"mappings":"AAAA,cAAc,mBAAmB,CAAC;AAClC,cAAc,0BAA0B,CAAC;AACzC,cAAc,cAAc,CAAC;AAC7B,cAAc,eAAe,CAAC;AAC9B,cAAc,aAAa,CAAC;AAC5B,cAAc,eAAe,CAAC;AAC9B,cAAc,mBAAmB,CAAC;AAClC,cAAc,WAAW,CAAC;AAC1B,cAAc,qBAAqB,CAAC;AACpC,cAAc,+BAA+B,CAAC;AAE9C,cAAc,sBAAsB,CAAC;AACrC,cAAc,wBAAwB,CAAC","sourcesContent":["export * from \"./generated/v3.js\";\nexport * from \"./generated/v3-models.js\";\nexport * from \"./runtime.js\";\nexport * from \"./metadata.js\";\nexport * from \"./errors.js\";\nexport * from \"./encoding.js\";\nexport * from \"./verification.js\";\nexport * from \"./bulk.js\";\nexport * from \"./subassessments.js\";\nexport * from \"./enrollment-verifications.js\";\n\nexport * from \"./gwt-enrollments.js\";\nexport * from \"./program-discovery.js\";\n"]}
|
|
1
|
+
{"version":3,"file":"v3.js","sourceRoot":"","sources":["../../src/v3.ts"],"names":[],"mappings":"AAAA,cAAc,mBAAmB,CAAC;AAClC,cAAc,0BAA0B,CAAC;AACzC,cAAc,cAAc,CAAC;AAC7B,cAAc,eAAe,CAAC;AAC9B,cAAc,aAAa,CAAC;AAC5B,cAAc,eAAe,CAAC;AAC9B,cAAc,mBAAmB,CAAC;AAClC,cAAc,WAAW,CAAC;AAC1B,cAAc,qBAAqB,CAAC;AACpC,cAAc,+BAA+B,CAAC;AAE9C,cAAc,sBAAsB,CAAC;AACrC,cAAc,wBAAwB,CAAC;AACvC,cAAc,uBAAuB,CAAC;AACtC,cAAc,mBAAmB,CAAC","sourcesContent":["export * from \"./generated/v3.js\";\nexport * from \"./generated/v3-models.js\";\nexport * from \"./runtime.js\";\nexport * from \"./metadata.js\";\nexport * from \"./errors.js\";\nexport * from \"./encoding.js\";\nexport * from \"./verification.js\";\nexport * from \"./bulk.js\";\nexport * from \"./subassessments.js\";\nexport * from \"./enrollment-verifications.js\";\n\nexport * from \"./gwt-enrollments.js\";\nexport * from \"./program-discovery.js\";\nexport * from \"./mobile-preflight.js\";\nexport * from \"./write-labels.js\";\n"]}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { AssessmentDefinition } from './metadata.js';
|
|
2
|
+
import type { EncodingOptions } from './encoding.js';
|
|
3
|
+
export interface WriteLabelObservation {
|
|
4
|
+
tenant: string;
|
|
5
|
+
response: unknown;
|
|
6
|
+
}
|
|
7
|
+
export type ReadBackWriteLabelResolver = NonNullable<EncodingOptions['writeLabel']> & {
|
|
8
|
+
readonly tenant: string;
|
|
9
|
+
};
|
|
10
|
+
/** Builds labels only from exact readback evidence. It never infers HUD suffixes.
|
|
11
|
+
* Unobserved or changed options return undefined, so the standard encoder refuses them. */
|
|
12
|
+
export declare function createReadBackWriteLabelResolver(definition: AssessmentDefinition, observations: readonly WriteLabelObservation[]): ReadBackWriteLabelResolver;
|
|
13
|
+
//# sourceMappingURL=write-labels.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"write-labels.d.ts","sourceRoot":"","sources":["../../src/write-labels.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,oBAAoB,EAAmC,MAAM,eAAe,CAAC;AAE1F,OAAO,KAAK,EAAC,eAAe,EAAC,MAAM,eAAe,CAAC;AAInD,MAAM,WAAW,qBAAqB;IAAE,MAAM,EAAC,MAAM,CAAC;IAAA,QAAQ,EAAC,OAAO,CAAC;CAAC;AACxE,MAAM,MAAM,0BAA0B,GAAG,WAAW,CAAC,eAAe,CAAC,YAAY,CAAC,CAAC,GAAG;IAAC,QAAQ,CAAC,MAAM,EAAC,MAAM,CAAA;CAAC,CAAC;AAE/G;2FAC2F;AAC3F,wBAAgB,gCAAgC,CAC/C,UAAU,EAAC,oBAAoB,EAC/B,YAAY,EAAC,SAAS,qBAAqB,EAAE,GAC5C,0BAA0B,CA2C3B"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { flattenQuestions } from './metadata.js';
|
|
2
|
+
import { ValidationError } from './errors.js';
|
|
3
|
+
import { isRecord } from './validation.js';
|
|
4
|
+
/** Builds labels only from exact readback evidence. It never infers HUD suffixes.
|
|
5
|
+
* Unobserved or changed options return undefined, so the standard encoder refuses them. */
|
|
6
|
+
export function createReadBackWriteLabelResolver(definition, observations) {
|
|
7
|
+
const questions = new Map();
|
|
8
|
+
for (const { question } of flattenQuestions(definition.questions)) {
|
|
9
|
+
if (question.questionType !== 'lookup')
|
|
10
|
+
continue;
|
|
11
|
+
const prior = questions.get(question.questionId);
|
|
12
|
+
if (prior && JSON.stringify(prior) !== JSON.stringify(question))
|
|
13
|
+
throw new ValidationError('ambiguous write-label question');
|
|
14
|
+
questions.set(question.questionId, structuredClone(question));
|
|
15
|
+
}
|
|
16
|
+
const labels = new Map();
|
|
17
|
+
const identity = (questionId, optionId) => JSON.stringify([questionId, optionId]);
|
|
18
|
+
const visit = (value, depth = 0) => {
|
|
19
|
+
if (depth > 40)
|
|
20
|
+
throw new ValidationError('write-label observation exceeds nesting limit');
|
|
21
|
+
if (Array.isArray(value)) {
|
|
22
|
+
for (const child of value)
|
|
23
|
+
visit(child, depth + 1);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
if (!isRecord(value))
|
|
27
|
+
return;
|
|
28
|
+
if (value.question_type === 'lookup' && typeof value.question_id === 'number') {
|
|
29
|
+
const question = questions.get(value.question_id);
|
|
30
|
+
if (question) {
|
|
31
|
+
if (value.virt_field_name !== question.fieldName)
|
|
32
|
+
throw new ValidationError('write-label question identity mismatch');
|
|
33
|
+
const raw = value.answer_value;
|
|
34
|
+
const id = typeof raw === 'number' ? raw : typeof raw === 'string' && /^\d+$/.test(raw) ? Number(raw) : null;
|
|
35
|
+
if (id !== null && Number.isSafeInteger(id) && typeof value.answer_value_text === 'string' && value.answer_value_text.trim()) {
|
|
36
|
+
const options = question.options.filter(option => option.id === id);
|
|
37
|
+
if (options.length !== 1)
|
|
38
|
+
throw new ValidationError('write-label option identity mismatch', [question.fieldName ?? String(question.questionId)]);
|
|
39
|
+
const key = identity(question.questionId, id), prior = labels.get(key);
|
|
40
|
+
if (prior !== undefined && prior !== value.answer_value_text)
|
|
41
|
+
throw new ValidationError('conflicting write-label observations', [question.fieldName ?? String(question.questionId)]);
|
|
42
|
+
labels.set(key, value.answer_value_text);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
// Traverse known response/assessment containers only. Do not interpret arbitrary strings as data.
|
|
47
|
+
for (const key of ['value', 'values', 'sub_assessment_question_list', 'sub_assessment_answer_list', 'sub_assessment_list'])
|
|
48
|
+
if (value[key] !== undefined)
|
|
49
|
+
visit(value[key], depth + 1);
|
|
50
|
+
};
|
|
51
|
+
for (const observation of observations) {
|
|
52
|
+
if (observation.tenant !== definition.tenant)
|
|
53
|
+
throw new ValidationError('write-label observation tenant mismatch');
|
|
54
|
+
visit(observation.response);
|
|
55
|
+
}
|
|
56
|
+
const sameOption = (a, b) => a.id === b.id && a.picklistId === b.picklistId && a.code === b.code && a.picklistCode === b.picklistCode && a.name === b.name && a.hudVisibility === b.hudVisibility;
|
|
57
|
+
const resolver = (question, option) => {
|
|
58
|
+
const known = questions.get(question.questionId);
|
|
59
|
+
if (!known || known.fieldName !== question.fieldName || question.questionType !== 'lookup' || !known.options.some(o => sameOption(o, option)))
|
|
60
|
+
return undefined;
|
|
61
|
+
return labels.get(identity(question.questionId, option.id));
|
|
62
|
+
};
|
|
63
|
+
return Object.freeze(Object.assign(resolver, { tenant: definition.tenant }));
|
|
64
|
+
}
|
|
65
|
+
//# sourceMappingURL=write-labels.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"write-labels.js","sourceRoot":"","sources":["../../src/write-labels.ts"],"names":[],"mappings":"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","sourcesContent":["import type {AssessmentDefinition,PicklistOption,QuestionDefinition} from './metadata.js';\nimport {flattenQuestions} from './metadata.js';\nimport type {EncodingOptions} from './encoding.js';\nimport {ValidationError} from './errors.js';\nimport {isRecord} from './validation.js';\n\nexport interface WriteLabelObservation {tenant:string;response:unknown;}\nexport type ReadBackWriteLabelResolver = NonNullable<EncodingOptions['writeLabel']> & {readonly tenant:string};\n\n/** Builds labels only from exact readback evidence. It never infers HUD suffixes.\n * Unobserved or changed options return undefined, so the standard encoder refuses them. */\nexport function createReadBackWriteLabelResolver(\n definition:AssessmentDefinition,\n observations:readonly WriteLabelObservation[],\n):ReadBackWriteLabelResolver{\n const questions=new Map<number,QuestionDefinition>();\n for(const {question} of flattenQuestions(definition.questions)){\n if(question.questionType!=='lookup')continue;\n const prior=questions.get(question.questionId);\n if(prior&&JSON.stringify(prior)!==JSON.stringify(question))throw new ValidationError('ambiguous write-label question');\n questions.set(question.questionId,structuredClone(question));\n }\n const labels=new Map<string,string>();\n const identity=(questionId:number,optionId:number)=>JSON.stringify([questionId,optionId]);\n const visit=(value:unknown,depth=0):void=>{\n if(depth>40)throw new ValidationError('write-label observation exceeds nesting limit');\n if(Array.isArray(value)){for(const child of value)visit(child,depth+1);return;}\n if(!isRecord(value))return;\n if(value.question_type==='lookup'&&typeof value.question_id==='number'){\n const question=questions.get(value.question_id);\n if(question){\n if(value.virt_field_name!==question.fieldName)throw new ValidationError('write-label question identity mismatch');\n const raw=value.answer_value;\n const id=typeof raw==='number'?raw:typeof raw==='string'&&/^\\d+$/.test(raw)?Number(raw):null;\n if(id!==null&&Number.isSafeInteger(id)&&typeof value.answer_value_text==='string'&&value.answer_value_text.trim()){\n const options=question.options.filter(option=>option.id===id);\n if(options.length!==1)throw new ValidationError('write-label option identity mismatch',[question.fieldName??String(question.questionId)]);\n const key=identity(question.questionId,id),prior=labels.get(key);\n if(prior!==undefined&&prior!==value.answer_value_text)throw new ValidationError('conflicting write-label observations',[question.fieldName??String(question.questionId)]);\n labels.set(key,value.answer_value_text);\n }\n }\n }\n // Traverse known response/assessment containers only. Do not interpret arbitrary strings as data.\n for(const key of ['value','values','sub_assessment_question_list','sub_assessment_answer_list','sub_assessment_list'])if(value[key]!==undefined)visit(value[key],depth+1);\n };\n for(const observation of observations){\n if(observation.tenant!==definition.tenant)throw new ValidationError('write-label observation tenant mismatch');\n visit(observation.response);\n }\n const sameOption=(a:PicklistOption,b:PicklistOption)=>a.id===b.id&&a.picklistId===b.picklistId&&a.code===b.code&&a.picklistCode===b.picklistCode&&a.name===b.name&&a.hudVisibility===b.hudVisibility;\n const resolver=(question:QuestionDefinition,option:PicklistOption)=>{\n const known=questions.get(question.questionId);\n if(!known||known.fieldName!==question.fieldName||question.questionType!=='lookup'||!known.options.some(o=>sameOption(o,option)))return undefined;\n return labels.get(identity(question.questionId,option.id));\n };\n return Object.freeze(Object.assign(resolver,{tenant:definition.tenant}));\n}\n"]}
|
package/docs/assessments.md
CHANGED
|
@@ -29,7 +29,23 @@ The default metadata cache TTL is 60 seconds with 100 entries per client. Config
|
|
|
29
29
|
|
|
30
30
|
WellSky requires a client context and known assessment code for these reads. There is no documented global assessment-list or standalone picklist-list endpoint. Mobile option metadata has no effective-date filter, so definitions explicitly identify its options as current even when the generic assessment read is dated. Metadata also does not provide a complete questionnaire branching engine.
|
|
31
31
|
|
|
32
|
-
Use the raw `assessments.get`, `assessmentsMobile.getAnswers`, and `enrollments.getAssessment` methods for recorded answers. Use `encodeAnswers` and `encodeNewSubassessmentRow` for endpoint-specific encoding. Mobile lookups use option IDs; standalone/enrollment lookups require an explicit `writeLabel` resolver with tenant-verified labels. Raw option names are not assumed to be valid write labels. Provider answers are refused for mobile writes because that endpoint can silently discard them. Answer-state code mappings and multi-value serialization are caller-supplied; a refusal is never converted to `No`.
|
|
32
|
+
Use the raw `assessments.get`, `assessmentsMobile.getAnswers`, and `enrollments.getAssessment` methods for recorded answers. Use `encodeAnswers` and `encodeNewSubassessmentRow` for endpoint-specific encoding. Mobile lookups use option IDs; standalone/enrollment lookups require an explicit `writeLabel` resolver with tenant-verified labels. Raw option names are not assumed to be valid write labels. Some HUD-marked choices require `(HUD)` in the full write label even when metadata `name` omits it. Do not infer this from trailing whitespace or append the suffix to every option. Continue supplying tenant-verified labels until a label resolver has authoritative metadata; recorded `answer_value_text` is useful evidence for the corresponding option. Provider answers are refused for mobile writes because that endpoint can silently discard them. Answer-state code mappings and multi-value serialization are caller-supplied; a refusal is never converted to `No`.
|
|
33
|
+
|
|
34
|
+
## Resolve exact lookup write labels from readback
|
|
35
|
+
|
|
36
|
+
`createReadBackWriteLabelResolver` preserves the exact `answer_value_text` for an observed question/option pair, including a HUD suffix and whitespace. It does not infer labels from trailing spaces or `hudVisibility`: retained readbacks include suffixed and unsuffixed labels with the same visibility flag.
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
import {createReadBackWriteLabelResolver} from '@choiceapps/communityservices-sdk';
|
|
40
|
+
const writeLabel = createReadBackWriteLabelResolver(definition, [
|
|
41
|
+
{tenant: api.tenant, response: recordedAnswers},
|
|
42
|
+
]);
|
|
43
|
+
// Supply writeLabel to encodeAnswers or createEnrollmentWorkflow.
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Supply authorized assessment reads from the definition's tenant. The helper matches question ID, field name and option ID, rejects conflicting labels and mixed tenants, and returns an immutable resolver tagged with its tenant. The enrollment workflow checks that tenant. The resolver returns `undefined` for unobserved or changed options, so normal encoding refuses them. Refresh the definition and observations after metadata changes.
|
|
47
|
+
|
|
48
|
+
This is an evidence-backed resolver, not a complete label catalog. Unobserved options still need an authoritative readback or another independently verified resolver. Do not write answers merely to discover labels. See [the typed example](../examples/checked-assessment-writes.mts).
|
|
33
49
|
|
|
34
50
|
## Yes/no answer acceptance
|
|
35
51
|
|
|
@@ -48,6 +64,27 @@ Reproduced on November 2, 2026 in a sandbox tenant with synthetic clients:
|
|
|
48
64
|
|
|
49
65
|
The enrollment workflow writes yes/no answers through the mobile batch by default. That keeps their stage pending because the saved values never verify. Construct it with `yesNoWrites: 'standard'` and a `writeLabel` resolver to send them through `assessments.create` instead; see [the enrollment workflow](enrollments.md#yesno-answers). It never treats an empty yes/no answer as `No`, skips a requested field, or switches endpoints for a stage that was already attempted. For a minimal reproduction, [enable private error diagnostics](troubleshooting.md#inspect-an-upstream-error-privately) and retain the question's current metadata.
|
|
50
66
|
|
|
67
|
+
## Shared questions across assessment instruments
|
|
68
|
+
|
|
69
|
+
An entry assessment and a standalone form can reference the same stored question. Changing `assessmentCode` does not make a second answer at the same client/question/effective instant available for a mobile insert. The integrator reported this for `HPTARGETINGSCORETOTAL` on an entry and form 1002: when the stored value differed, the mobile batch was rejected. Compare question identifiers and effective dates, not only form names.
|
|
70
|
+
|
|
71
|
+
Read the existing answer before planning a mobile scalar insert. If it already exists at that instant with another value, use a supported standard correction/update path after resolving the original operation; do not move its timestamp to evade the collision. Raw `assessmentsMobile.addAnswer` remains a raw write and performs no implicit read or cross-form collision check. Never switch endpoints to retry an uncertain batch.
|
|
72
|
+
|
|
73
|
+
Use `writeMobileAnswersChecked(api, request)` for an explicit scalar collision check before a raw mobile batch. It accepts the ordinary mobile request with a required timezone-qualified, whole-second `query.dateEffective`. It reads the supplied instrument one second after that instant so yes/no rows are visible, then matches question identity and effective date. A row at the target instant blocks the entire batch even when its value is identical or empty. Missing, invalid or newer readback evidence also blocks the write. Older answers allow the insert.
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
import {writeMobileAnswersChecked, MobileAnswerPreflightError} from '@choiceapps/communityservices-sdk';
|
|
77
|
+
await writeMobileAnswersChecked(api, {
|
|
78
|
+
path: {clientId, assessmentCode},
|
|
79
|
+
query: {dateEffective: originalEffectiveInstant},
|
|
80
|
+
body: encodedMobileAnswers,
|
|
81
|
+
});
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
`MobileAnswerPreflightError` has a stable `code`, affected `fields`, and `committed: false`. A collision uses `MOBILE_ANSWER_EXISTS`; unavailable evidence uses `MOBILE_PREFLIGHT_UNAVAILABLE`. The helper checks scalars, including questions shared across instruments, but does not detect duplicate subassessment rows. It makes one write after a successful preflight and does not retry or journal it. An error from that write retains its original type and possible uncertain outcome. Reads and writes are not atomic against other users. Pass the original instant; the SDK transport applies any configured mobile date correction.
|
|
85
|
+
|
|
86
|
+
New enrollment workflow plans also check scalar collisions before creating an enrollment or sending a mobile batch. Existing pending journal operations retain their reconciliation behavior.
|
|
87
|
+
|
|
51
88
|
## Race and ethnicity combinations
|
|
52
89
|
|
|
53
90
|
`encodeAnswers` recognizes the standard `HUD_RACEETHNICITY` field. It resolves single-category and multiple-category answers to the tenant's `RACE_ETH_COMBO_*` option IDs. A single White answer uses `RACE_ETH_COMBO_WHI`, even when the caller supplies `RACE_ETH_WHI`. Atomic option IDs can read back with a label while appearing blank in the UI.
|
package/docs/browser-session.md
CHANGED
|
@@ -97,7 +97,7 @@ On the expired-login form, the helper appends the character to the original `HMI
|
|
|
97
97
|
|
|
98
98
|
Use one shared, durable recovery store for every caller of the same UI account. Multi-host deployments must override the default with shared durable storage and locking. Keep using an existing custom recovery store if it has a pending recovery; changing stores does not reconcile it. The per-account lock covers the entire recovery. Its journal contains stages and the append character, never either password. The original must be 8–49 characters when a reset is needed so the temporary password fits the observed 50-character limit. A 50-character password can still log in normally. Keep the original credential and append character unchanged while a recovery is pending. The SDK sanitizes login errors; do not enable Playwright tracing, request-body capture, or debug logging containing credentials.
|
|
99
99
|
|
|
100
|
-
A saved temporary-password attempt can resume with one authentication check of that temporary password. A saved restoration attempt can resume with one authentication check of the original. Neither uncertain password write is repeated. An interrupted or failed authentication check leaves a blocking journal stage and raises `RECOVERY_UNCERTAIN`; reconcile the account and journal before proceeding. A process crash may also leave the file store's exclusive lock. Do not delete journals or automatically expire locks to force retries.
|
|
100
|
+
A saved temporary-password attempt can resume with one authentication check of that temporary password. A saved restoration attempt can resume with one authentication check of the original. Neither uncertain password write is repeated. An interrupted or failed authentication check leaves a blocking journal stage and raises `RECOVERY_UNCERTAIN`; reconcile the account and journal before proceeding. A process crash may also leave the file store's exclusive lock. Do not delete journals or automatically expire locks to force retries. WellSky permits immediately resetting back to the previous password. Password history does not prevent the restore-original step.
|
|
101
101
|
|
|
102
102
|
Both UI actions use GWT-RPC: the shipped expired-login handler calls `AuthenticationService.changePasswordHeartbeat`, while the user-settings restoration was observed calling `UserService.updateUserPassword` with HTTP 200 / GWT OK. The former uses the limited session established by the expired login. The helper currently drives the live forms to generate fresh requests. A standalone RPC-only password adapter has not been implemented or tested; captured password payloads must not be stored or replayed. See [sandbox findings](capabilities.md).
|
|
103
103
|
|
package/docs/capabilities.md
CHANGED
|
@@ -39,7 +39,7 @@ The generated client covers the pinned V2/V3 Swagger operations. The following f
|
|
|
39
39
|
| Client Notes, goals, action steps | `createCasePlans` plus raw REST methods | Journaled create/update and supported-field verification; helper supplies clientId on goal updates; unreadable follow-up fields excluded |
|
|
40
40
|
| Goal case notes | `session.workflows.createCaseNote` | Optional source key rejects changed intent and verifies saved note ID; legacy unkeyed matching remains; single client only. REST goal response does not expose these notes |
|
|
41
41
|
| Multi-client/multi-case notes | No packaged bulk helper | Do not infer note participants from unrelated `households` nested in a goal response |
|
|
42
|
-
| Expired UI password | Headless login, enabled by default | Temporary suffix then restore original, durable recovery stages, fresh-login verification;
|
|
42
|
+
| Expired UI password | Headless login, enabled by default | Temporary suffix then restore original, durable recovery stages, fresh-login verification; WellSky permits immediate restoration of the previous password |
|
|
43
43
|
| Session expiration | Managed session plus durable workflow journals | Reauthenticate and report interruption; caller retries original intent for reconciliation, not automatic callback replay |
|
|
44
44
|
|
|
45
45
|
## Known server gaps
|
|
@@ -69,7 +69,7 @@ These observations explain the current contracts. They are dated acceptance resu
|
|
|
69
69
|
| Household exits and head coverage, October 23, 2026 | An invalid early head exit was blocked before writes. Valid staggered exits and an all-member exit preserved dates, original heads, and selected subgroups; retries made no extra record writes. | Earlier server-accepted orphaned groups were invalid under the current SDK contract. See [head validation feedback](vendor-issues/household-head-validation.md) and [enrollments](enrollments.md). |
|
|
70
70
|
| Attachment identity and content, October 24, 2026 | Profile, Case Plans, and goal files retained their IDs across fresh helpers, and downloaded bytes matched the originals. | Does not establish anonymous access or sharing permissions. REST authentication remained a separate failure. See [files and notes](files-and-notes.md). |
|
|
71
71
|
| SSVF conditional forms, October 23, 2026 | Form inspection checked conditional branches and required-date validation without saving; four REST-supported dependencies were then saved and read back, with zero duplicate writes on same-key retries. | This is a dated rules snapshot, not exhaustive financial assistance or funding-allocation acceptance. See [SSVF rules](../SSVF.md) and [services](services.md). |
|
|
72
|
-
| Expired-password recovery, October 23, 2026 | An expired account completed temporary-password recovery, restored its original password, and authenticated in a fresh context. |
|
|
72
|
+
| Expired-password recovery, October 23, 2026 | An expired account completed temporary-password recovery, restored its original password, and authenticated in a fresh context. | WellSky permits immediate restoration of the previous password; retain the durable recovery journal for interrupted operations. See [browser sessions](browser-session.md). |
|
|
73
73
|
|
|
74
74
|
Independent needs and AIRS/service-type discovery use UI/GWT helpers. See [independent needs](independent-needs.md) for the creation, counted catalog, and retry contracts. The pinned Swagger exposes need reads but no independent-need creation.
|
|
75
75
|
|
|
@@ -86,3 +86,16 @@ The integrator reported these results using SDK 0.4.1 in the training tenant. Th
|
|
|
86
86
|
- After those household exits, both re-enrollment attempts failed on the pre-create REST list with HTTP 400 from the grouped-enrollment `42703` gap. This confirms that an exited household enrollment can still break that REST list.
|
|
87
87
|
|
|
88
88
|
The GWT catalog correction has regression coverage for individual creation, household heads and member preflight, failed-read classification and re-enrollment. Same-date entry corrections and adoption of manual enrollments have synthetic coverage. These newer paths have not been independently verified live.
|
|
89
|
+
|
|
90
|
+
## Integrator-reported SDK 0.6.0 acceptance
|
|
91
|
+
|
|
92
|
+
The integrator reports these training-tenant results on November 10 and 11, 2026; the SDK team has not independently rerun them:
|
|
93
|
+
|
|
94
|
+
- GWT `listEnrollments` succeeded for a household client whose REST list returned HTTP 400.
|
|
95
|
+
- Standalone `adoptEnrollment` followed by `createInterim`, and single-person same-day `correctEntry`, succeeded. Same-key repeats made no writes.
|
|
96
|
+
- Three pending 0.5.0 corrections were refused without confirmation and resolved as `rejected` with confirmation.
|
|
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
|
+
|
|
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.
|
|
100
|
+
|
|
101
|
+
The 0.7.0 label resolver, group flags, household discovery, row registration, error codes and mobile collision preflight have local regression coverage, not new live tenant acceptance. Full write labels remain unresolved for unobserved options. Household discovery refuses ambiguous current-membership matches. Yes/no blanking remains unsupported; no alternative route has been verified.
|
package/docs/enrollments.md
CHANGED
|
@@ -148,14 +148,36 @@ const corrected = await workflow.correctEntry({
|
|
|
148
148
|
});
|
|
149
149
|
```
|
|
150
150
|
|
|
151
|
-
`answers` uses the ordinary answer and option selectors. `clear` explicitly removes a recorded value; an omitted field stays unchanged.
|
|
151
|
+
`answers` uses the ordinary answer and option selectors. `clear` explicitly removes a recorded value; an omitted field stays unchanged. Yes/no fields cannot appear in `clear`: the SDK throws `UNSUPPORTED_YES_NO_CLEAR`, with the affected `fields`, before any correction write or new journal record. Use `{value: false}` to answer `No`; it does not mean blank. No alternative yes/no blanking route has been verified. Do not use `notApplicable` to clear an old answer. Every row key must identify an existing row returned by the original entry operation or explicitly bound with `registerEntryRows`. The correction never creates a second row, deletes a row, or changes the enrollment date.
|
|
152
152
|
|
|
153
|
-
Scalars use the standard assessment endpoint, which supports same-date updates. The batch includes every mandatory scalar, preserving its current value unless the patch explicitly answers or clears it. Missing mandatory values require an explicit answer or clear; lookup values require verified write labels. Readback checks one second after the original instant so stored yes/no values are visible.
|
|
153
|
+
Scalars use the standard assessment endpoint, which supports same-date updates. The batch includes every mandatory scalar, preserving its current value unless the patch explicitly answers or clears it. Missing mandatory values require an explicit answer, or an explicit clear for a supported non-yes/no type; lookup values require verified write labels. Readback checks one second after the original instant so stored yes/no values are visible.
|
|
154
154
|
|
|
155
155
|
Row edits send only the changed cells to the existing recordset through the mobile endpoint. Lookup, text, numeric and date cells are supported; provider and yes/no row cells are refused before any write. Untouched cells, all other rows and row IDs must remain unchanged. DOB and household-head changes require their dedicated workflows and cannot be supplied as corrections here. An unmodified mandatory DOB is also refused because the standard batch cannot safely resend it through the DOB helper.
|
|
156
156
|
|
|
157
157
|
All changes are validated before the first write. Scalar and row batches have separate journal stages, so a partial correction is not rolled back. Retry the same intent and key to reconcile; an uncertain batch is never resent automatically. `getStatus({clientId, key})` reports correction progress. Verified corrections update the original entry's verification expectations, so retrying `createEnrollment` under its old key does not restore the old path. A later correction uses a new correction key. See the [typed example](../examples/entry-correction-workflow.mts).
|
|
158
158
|
|
|
159
|
+
## Resolve a rejected correction
|
|
160
|
+
|
|
161
|
+
An attempted correction stays pending until its outcome is established. A 4xx response or an unchanged read alone does not prove non-application, so the SDK does not automatically reset or resend it. New failures expose `code: 'ENTRY_CORRECTION_UNCERTAIN'`, `step`, and an available HTTP `status`. The first failure retains its original `cause`; a failed reconciliation read retains both errors in an `AggregateError`. Only allowlisted failure kind/status are persisted and returned by `getStatus`. Response bodies, messages, credentials and stacks are not written to the journal. Older journals cannot recover diagnostics they never saved.
|
|
162
|
+
|
|
163
|
+
If independent server or administrative evidence establishes that an attempted correction was not applied, call:
|
|
164
|
+
|
|
165
|
+
```ts
|
|
166
|
+
await workflow.resolveEntryCorrection({
|
|
167
|
+
key: failedCorrectionKey,
|
|
168
|
+
enrollmentKey,
|
|
169
|
+
clientId,
|
|
170
|
+
evidenceReference: independentReviewReference,
|
|
171
|
+
confirmNotApplied: async evidence => rejectionReview.confirmNotApplied(evidence),
|
|
172
|
+
});
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
`rejectionReview` is application-owned. Its callback must be read-only and confirm non-application from independent evidence; do not implement it as `() => true` or infer its answer only from HTTP status and `matchesBefore`. The callback receives identities, effective date and sanitized step diagnostics. Do not call the same client's workflow methods from the callback: the client journal lock remains held.
|
|
176
|
+
|
|
177
|
+
The SDK verifies the complete saved pre-write state before and after confirmation and rechecks enrollment context. A partial application, changed state or unavailable read prevents resolution. Successful resolution marks the correction `rejected` while retaining its original intent, attempted steps, diagnostics and evidence reference. It makes no SDK record writes to WellSky. Repeating the same resolution returns `already-rejected`; submitting that correction key again throws `ENTRY_CORRECTION_REJECTED`. Use a new correction key for the corrected intent only after resolution. A verified successful correction cannot be rejected.
|
|
178
|
+
|
|
179
|
+
This also supports pending 0.5.0 journals, including those without failure metadata, when the required independent evidence exists. For these journals the callback can receive only a step name and state, with no failure kind or status. The caller must retain its own original failure evidence; the SDK cannot reconstruct it and must not resend the write to obtain diagnostics. A new validation failure that occurs before journaling needs no resolution.
|
|
180
|
+
|
|
159
181
|
## Household enrollment and interim assessments
|
|
160
182
|
|
|
161
183
|
Enrollment creation and member preflight use the complete GWT list when the configured `enrollmentSession` supplies `listEnrollments`. The built-in sessions supply it. This avoids the REST `42703` failure for clients with existing household enrollments, including exited enrollments and enrollments in other programs. Reads verify the client, tenant, row identities and count; a failed GWT read never falls back to REST or means an empty list. The current GWT reader refuses lists above 1,000 records. Legacy custom readers without this method retain the counted REST path.
|
|
@@ -196,6 +218,22 @@ Client locks coordinate overlapping household and single-client operations. Dura
|
|
|
196
218
|
|
|
197
219
|
See the typed [household example](../examples/household-enrollment-workflow.mts) and [live household verification](capabilities.md). The integration uses the configured tenant and uses an isolated authenticated page.
|
|
198
220
|
|
|
221
|
+
## Public enrollment reads
|
|
222
|
+
|
|
223
|
+
The managed session exposes a GWT-backed catalog independently of the workflow:
|
|
224
|
+
|
|
225
|
+
```ts
|
|
226
|
+
const {values: enrollments} =
|
|
227
|
+
await session.enrollmentSession.listEnrollments({clientId});
|
|
228
|
+
|
|
229
|
+
const detail = await session.enrollmentSession.readEnrollment({
|
|
230
|
+
clientId: headClientId, enrollmentId: headEnrollmentId,
|
|
231
|
+
});
|
|
232
|
+
// detail.members contains the complete linked clientId/enrollmentId pairs.
|
|
233
|
+
```
|
|
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.
|
|
236
|
+
|
|
199
237
|
## Individual enrollment exits
|
|
200
238
|
|
|
201
239
|
Use `exitEnrollment` to end a standalone person's program enrollment. It requires a verified `createEnrollment` entry in the same durable store. Supply `enrollmentSession: session.enrollmentSession` and `exitUi: createHouseholdExitUi({page: session.page})` when constructing the workflow. The same exit UI port serves individual and household forms.
|
|
@@ -226,7 +264,41 @@ Use `adoptEnrollment({key, clientId, enrollmentId})` for a standalone enrollment
|
|
|
226
264
|
|
|
227
265
|
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.
|
|
228
266
|
|
|
229
|
-
After adoption,
|
|
267
|
+
After standalone adoption, its key can be used as `enrollmentKey` for `createInterim`, scalar-only `correctEntry`, or `exitEnrollment`. After household adoption, the group key works with `createHouseholdInterim` and `exitHousehold`; use the returned member's `key` for that person's scalar-only `correctEntry`. Correction still requires an open entry without reviews or exits. Do not use an all-not-applicable `createEnrollment` call merely to link an existing entry: it can create an enrollment when no match exists. Exit assessment plans and normal household head rules still apply. Adoption initially returns empty `recordsetIds`. Use `registerEntryRows` below to bind verified existing recordsets before row corrections; a new invented key alone cannot identify a historical row. See [manual-entry exit examples](../examples/existing-enrollment-exits.mts).
|
|
268
|
+
|
|
269
|
+
### Discover a household from its head enrollment
|
|
270
|
+
|
|
271
|
+
```ts
|
|
272
|
+
const discovered = await workflow.discoverHouseholdEnrollment({
|
|
273
|
+
headClientId, headEnrollmentId,
|
|
274
|
+
});
|
|
275
|
+
// discovered: householdId, householdSource, providerId, type, members
|
|
276
|
+
const adopted = await workflow.adoptHouseholdEnrollment({
|
|
277
|
+
key: sourceEnrollmentKey, headClientId, headEnrollmentId,
|
|
278
|
+
});
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
Discovery reads the linked enrollment cohort through GWT and matches it against complete current household membership reads. It requires one household containing the cohort, the same head, and membership dates covering each entry. It verifies that household again before returning `householdSource: 'verified-membership'`. This is a verified membership match, not a native historical household pointer on the enrollment. Zero matches, multiple matches, incomplete reads or changed relationships fail without adopting. Historical households that no longer match require the existing explicit adoption inputs and their validation. No record is written to WellSky during discovery or adoption.
|
|
282
|
+
|
|
283
|
+
### Register existing entry rows
|
|
284
|
+
|
|
285
|
+
```ts
|
|
286
|
+
await workflow.registerEntryRows({
|
|
287
|
+
key: sourceRegistrationKey,
|
|
288
|
+
enrollmentKey: adoptedEntryKey,
|
|
289
|
+
clientId,
|
|
290
|
+
rows: [{key: 'entry-ce', parentField, recordsetId: existingRecordsetId}],
|
|
291
|
+
});
|
|
292
|
+
// correctEntry can now reference key: 'entry-ce' with the same parentField.
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
Select explicit recordset IDs from authorized assessment reads. Registration verifies the enrollment, question definition, and each row's presence in both entry-time and current reads. It stores immutable key/parent/recordset bindings in the local journal and makes no WellSky record writes. Same-key repeats return `already-present`; duplicate aliases, absent rows, rebinding, and pending operations are refused. Native entry row keys already work without registration. For household adoption, use each returned member's entry `key` and register that member's rows separately. Registration does not establish who originally created a row or make later edits eligible: `correctEntry` still enforces open-entry and no-review rules. See [the typed example](../examples/enrollment-discovery-and-correction.mts).
|
|
296
|
+
|
|
297
|
+
### Classify workflow failures
|
|
298
|
+
|
|
299
|
+
`EnrollmentWorkflowError` exposes a stable `code`, `operation`, and `committed`. Classify by code instead of message text. Pre-write validation, discovery and adoption failures report `committed: false`. Any attempted write, relevant prior attempt, or unreadable prior outcome remains `'unknown'`. The marker describes WellSky record writes for that operation, not local journal changes. `cause` preserves the original error where available.
|
|
300
|
+
|
|
301
|
+
A failed `resolveEntryCorrection` can have `committed: false` because resolution itself writes no WellSky records, while `originalCommitted: 'unknown'` describes the original correction. Never use a resolution error's marker to infer non-application of the original write. See [error codes and recovery](troubleshooting.md#workflow-error-codes).
|
|
230
302
|
|
|
231
303
|
## Household exits
|
|
232
304
|
|