jsfe 1.6.0 → 1.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -4
- package/dist/forms/action-projection.d.ts +4 -0
- package/dist/forms/action-projection.d.ts.map +1 -0
- package/dist/forms/action-projection.js +21 -0
- package/dist/forms/action-recovery.d.ts +12 -0
- package/dist/forms/action-recovery.d.ts.map +1 -0
- package/dist/forms/action-recovery.js +125 -0
- package/dist/forms/action-tools.d.ts +29 -0
- package/dist/forms/action-tools.d.ts.map +1 -0
- package/dist/forms/action-tools.js +6 -0
- package/dist/forms/address-flows.d.ts +2 -0
- package/dist/forms/address-flows.d.ts.map +1 -0
- package/dist/forms/address-flows.js +39 -0
- package/dist/forms/choice-flow.d.ts +419 -0
- package/dist/forms/choice-flow.d.ts.map +1 -0
- package/dist/forms/choice-flow.js +707 -0
- package/dist/forms/contracts.d.ts +237 -0
- package/dist/forms/contracts.d.ts.map +1 -0
- package/dist/forms/contracts.js +278 -0
- package/dist/forms/flows.d.ts +611 -0
- package/dist/forms/flows.d.ts.map +1 -0
- package/dist/forms/flows.js +675 -0
- package/dist/forms/identity-flows.d.ts +17 -0
- package/dist/forms/identity-flows.d.ts.map +1 -0
- package/dist/forms/identity-flows.js +302 -0
- package/dist/forms/index.d.ts +7 -0
- package/dist/forms/index.d.ts.map +1 -0
- package/dist/forms/index.js +10 -0
- package/dist/forms/prompt-compiler.d.ts +3 -0
- package/dist/forms/prompt-compiler.d.ts.map +1 -0
- package/dist/forms/prompt-compiler.js +30 -0
- package/dist/forms/session-actions.d.ts +14 -0
- package/dist/forms/session-actions.d.ts.map +1 -0
- package/dist/forms/session-actions.js +40 -0
- package/dist/helpers/address.d.ts +27 -0
- package/dist/helpers/address.d.ts.map +1 -0
- package/dist/helpers/address.js +29 -0
- package/dist/helpers/answer.d.ts +5 -0
- package/dist/helpers/answer.d.ts.map +1 -1
- package/dist/helpers/answer.js +5 -3
- package/dist/helpers/choice-offer.d.ts +3 -0
- package/dist/helpers/choice-offer.d.ts.map +1 -0
- package/dist/helpers/choice-offer.js +6 -0
- package/dist/helpers/form-actions.d.ts +13 -0
- package/dist/helpers/form-actions.d.ts.map +1 -0
- package/dist/helpers/form-actions.js +144 -0
- package/dist/helpers/form-sources.d.ts +7 -0
- package/dist/helpers/form-sources.d.ts.map +1 -0
- package/dist/helpers/form-sources.js +47 -0
- package/dist/helpers/form-state.d.ts +94 -0
- package/dist/helpers/form-state.d.ts.map +1 -0
- package/dist/helpers/form-state.js +870 -0
- package/dist/helpers/index.d.ts +114 -1
- package/dist/helpers/index.d.ts.map +1 -1
- package/dist/helpers/index.js +9 -4
- package/dist/helpers/match-inputs.d.ts +14 -0
- package/dist/helpers/match-inputs.d.ts.map +1 -1
- package/dist/helpers/match-inputs.js +154 -41
- package/dist/helpers/money.d.ts +12 -0
- package/dist/helpers/money.d.ts.map +1 -0
- package/dist/helpers/money.js +54 -0
- package/dist/helpers/spoken-money.d.ts +20 -0
- package/dist/helpers/spoken-money.d.ts.map +1 -0
- package/dist/helpers/spoken-money.js +143 -0
- package/dist/host/action-journal.d.ts +98 -0
- package/dist/host/action-journal.d.ts.map +1 -0
- package/dist/host/action-journal.js +218 -0
- package/dist/host/action-session.d.ts +12 -0
- package/dist/host/action-session.d.ts.map +1 -0
- package/dist/host/action-session.js +29 -0
- package/dist/host/capabilities.d.ts +14 -0
- package/dist/host/capabilities.d.ts.map +1 -0
- package/dist/host/capabilities.js +60 -0
- package/dist/host/checkpoint-codec-error.d.ts +6 -0
- package/dist/host/checkpoint-codec-error.d.ts.map +1 -0
- package/dist/host/checkpoint-codec-error.js +4 -0
- package/dist/host/index.d.ts +6 -0
- package/dist/host/index.d.ts.map +1 -1
- package/dist/host/index.js +6 -0
- package/dist/host/intent-stages.d.ts +1 -0
- package/dist/host/intent-stages.d.ts.map +1 -1
- package/dist/host/intent-stages.js +3 -3
- package/dist/host/node.d.ts +17 -0
- package/dist/host/node.d.ts.map +1 -0
- package/dist/host/node.js +64 -0
- package/dist/host/recovery-identity.d.ts +16 -0
- package/dist/host/recovery-identity.d.ts.map +1 -0
- package/dist/host/recovery-identity.js +73 -0
- package/dist/host/recovery-policy.d.ts +5 -0
- package/dist/host/recovery-policy.d.ts.map +1 -0
- package/dist/host/recovery-policy.js +54 -0
- package/dist/host/runtime-manifest.d.ts +3 -0
- package/dist/host/runtime-manifest.d.ts.map +1 -0
- package/dist/host/runtime-manifest.js +38 -0
- package/dist/index.d.ts +61 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +186 -45
- package/dist/runtime-identity.d.ts +3 -0
- package/dist/runtime-identity.d.ts.map +1 -0
- package/dist/runtime-identity.js +1 -0
- package/docs/development-release.md +25 -0
- package/docs/forms.md +132 -0
- package/package.json +15 -4
package/dist/helpers/index.d.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
export namespace STANDARD_FUNCTIONS {
|
|
2
|
+
export { readMoneyAmount };
|
|
3
|
+
export { isMoneyAmount };
|
|
4
|
+
export { moneyIntentAllows };
|
|
5
|
+
export { validateMoneyOptions };
|
|
6
|
+
export { takeChoiceOffer };
|
|
2
7
|
export { matchesChoice };
|
|
3
8
|
export { isAnswer };
|
|
4
9
|
export { isAgentRequest };
|
|
@@ -19,11 +24,63 @@ export namespace STANDARD_FUNCTIONS {
|
|
|
19
24
|
export { normalizeAndFindCapture };
|
|
20
25
|
export { anythingElse };
|
|
21
26
|
export { goodbye };
|
|
27
|
+
export { sayName };
|
|
22
28
|
export { dateToWords };
|
|
23
29
|
export { amountToSpeech };
|
|
24
30
|
export { validateCardNumber };
|
|
25
31
|
export { validateExpiration };
|
|
32
|
+
export { consumeActionReceipt };
|
|
33
|
+
export { authorizeFormActionRetry };
|
|
34
|
+
export { formSignature };
|
|
35
|
+
export { selectFormAction };
|
|
36
|
+
export { confirmFormRevision };
|
|
37
|
+
export { prepareFormAction };
|
|
38
|
+
export { actionConfirmationValid };
|
|
39
|
+
export { beginFormAction };
|
|
40
|
+
export { finishFormAction };
|
|
41
|
+
export { actionGateVerified };
|
|
42
|
+
export { sourceNeedsLoad };
|
|
43
|
+
export { beginSourceRefresh };
|
|
44
|
+
export { completeSourceRefresh };
|
|
45
|
+
export { pickProviderQuestion };
|
|
26
46
|
export { matchInputs };
|
|
47
|
+
export { readFormAnswer };
|
|
48
|
+
export { readInputProfile };
|
|
49
|
+
export { resolveFormInputs };
|
|
50
|
+
export { formInputApplies };
|
|
51
|
+
export { formInputFilled };
|
|
52
|
+
export { nextFormInput };
|
|
53
|
+
export { formInputChain };
|
|
54
|
+
export { openFormInstance };
|
|
55
|
+
export { validateFormIncompleteOutcome };
|
|
56
|
+
export { formIncompleteOutcome };
|
|
57
|
+
export { validateFormExhaustion };
|
|
58
|
+
export { exhaustFormInput };
|
|
59
|
+
export { formInitialReadings };
|
|
60
|
+
export { orderFormReadings };
|
|
61
|
+
export { proposedFormValues };
|
|
62
|
+
export { applyFormValidation };
|
|
63
|
+
export { formReadingDependencies };
|
|
64
|
+
export { exclusiveFormReadings };
|
|
65
|
+
export { enrichFormMatches };
|
|
66
|
+
export { expandFormActivations };
|
|
67
|
+
export { settleFormActivations };
|
|
68
|
+
export { rememberFormArguments };
|
|
69
|
+
export { resetFormInputs };
|
|
70
|
+
export { hasFormValue };
|
|
71
|
+
export { formPath };
|
|
72
|
+
export { formValuesEqual };
|
|
73
|
+
export { formReadingReady };
|
|
74
|
+
export { formReadingRelevant };
|
|
75
|
+
export { formPreparation };
|
|
76
|
+
export { queueFormReadings };
|
|
77
|
+
export { pendingFormReadings };
|
|
78
|
+
export { formWorkLimit };
|
|
79
|
+
export { rejectFormReadings };
|
|
80
|
+
export { invalidateFormInputs };
|
|
81
|
+
export { acceptFormReadings };
|
|
82
|
+
export { formInstanceApplies };
|
|
83
|
+
export { closeFormInstance };
|
|
27
84
|
export { validateOTP };
|
|
28
85
|
export { validateDigits };
|
|
29
86
|
export { validatePhone };
|
|
@@ -31,6 +88,11 @@ export namespace STANDARD_FUNCTIONS {
|
|
|
31
88
|
export { urlToSpeech };
|
|
32
89
|
export { textWithUrlToSpeech };
|
|
33
90
|
}
|
|
91
|
+
import { readMoneyAmount } from './money.js';
|
|
92
|
+
import { isMoneyAmount } from './money.js';
|
|
93
|
+
import { moneyIntentAllows } from './money.js';
|
|
94
|
+
import { validateMoneyOptions } from './money.js';
|
|
95
|
+
import { takeChoiceOffer } from './choice-offer.js';
|
|
34
96
|
import { matchesChoice } from './matches-choice.js';
|
|
35
97
|
import { ANSWER_WORDS } from './answer.js';
|
|
36
98
|
import { CONFIRM_WORDS } from './answer.js';
|
|
@@ -58,7 +120,58 @@ import { dateToWords } from './date-words.js';
|
|
|
58
120
|
import { amountToSpeech } from './amount-to-speech.js';
|
|
59
121
|
import { validateCardNumber } from './card-checks.js';
|
|
60
122
|
import { validateExpiration } from './card-checks.js';
|
|
123
|
+
import { consumeActionReceipt } from './form-actions.js';
|
|
124
|
+
import { authorizeFormActionRetry } from './form-actions.js';
|
|
125
|
+
import { formSignature } from './form-actions.js';
|
|
126
|
+
import { selectFormAction } from './form-actions.js';
|
|
127
|
+
import { confirmFormRevision } from './form-actions.js';
|
|
128
|
+
import { prepareFormAction } from './form-actions.js';
|
|
129
|
+
import { actionConfirmationValid } from './form-actions.js';
|
|
130
|
+
import { beginFormAction } from './form-actions.js';
|
|
131
|
+
import { finishFormAction } from './form-actions.js';
|
|
132
|
+
import { actionGateVerified } from './form-actions.js';
|
|
133
|
+
import { sourceNeedsLoad } from './form-sources.js';
|
|
134
|
+
import { beginSourceRefresh } from './form-sources.js';
|
|
135
|
+
import { completeSourceRefresh } from './form-sources.js';
|
|
136
|
+
import { pickProviderQuestion } from './form-sources.js';
|
|
61
137
|
import { matchInputs } from './match-inputs.js';
|
|
138
|
+
import { readFormAnswer } from './match-inputs.js';
|
|
139
|
+
import { readInputProfile } from './match-inputs.js';
|
|
140
|
+
import { resolveFormInputs } from './form-state.js';
|
|
141
|
+
import { formInputApplies } from './form-state.js';
|
|
142
|
+
import { formInputFilled } from './form-state.js';
|
|
143
|
+
import { nextFormInput } from './form-state.js';
|
|
144
|
+
import { formInputChain } from './form-state.js';
|
|
145
|
+
import { openFormInstance } from './form-state.js';
|
|
146
|
+
import { validateFormIncompleteOutcome } from './form-state.js';
|
|
147
|
+
import { formIncompleteOutcome } from './form-state.js';
|
|
148
|
+
import { validateFormExhaustion } from './form-state.js';
|
|
149
|
+
import { exhaustFormInput } from './form-state.js';
|
|
150
|
+
import { formInitialReadings } from './form-state.js';
|
|
151
|
+
import { orderFormReadings } from './form-state.js';
|
|
152
|
+
import { proposedFormValues } from './form-state.js';
|
|
153
|
+
import { applyFormValidation } from './form-state.js';
|
|
154
|
+
import { formReadingDependencies } from './form-state.js';
|
|
155
|
+
import { exclusiveFormReadings } from './form-state.js';
|
|
156
|
+
import { enrichFormMatches } from './form-state.js';
|
|
157
|
+
import { expandFormActivations } from './form-state.js';
|
|
158
|
+
import { settleFormActivations } from './form-state.js';
|
|
159
|
+
import { rememberFormArguments } from './form-state.js';
|
|
160
|
+
import { resetFormInputs } from './form-state.js';
|
|
161
|
+
import { hasFormValue } from './form-state.js';
|
|
162
|
+
import { formPath } from './form-state.js';
|
|
163
|
+
import { formValuesEqual } from './form-state.js';
|
|
164
|
+
import { formReadingReady } from './form-state.js';
|
|
165
|
+
import { formReadingRelevant } from './form-state.js';
|
|
166
|
+
import { formPreparation } from './form-state.js';
|
|
167
|
+
import { queueFormReadings } from './form-state.js';
|
|
168
|
+
import { pendingFormReadings } from './form-state.js';
|
|
169
|
+
import { formWorkLimit } from './form-state.js';
|
|
170
|
+
import { rejectFormReadings } from './form-state.js';
|
|
171
|
+
import { invalidateFormInputs } from './form-state.js';
|
|
172
|
+
import { acceptFormReadings } from './form-state.js';
|
|
173
|
+
import { formInstanceApplies } from './form-state.js';
|
|
174
|
+
import { closeFormInstance } from './form-state.js';
|
|
62
175
|
import { checkOtp } from './otp-check.js';
|
|
63
176
|
import { maskDestination } from './otp-check.js';
|
|
64
177
|
import { validateOTP } from './otp-check.js';
|
|
@@ -68,5 +181,5 @@ import { validatePhone } from './validators.js';
|
|
|
68
181
|
import { validateEmail } from './validators.js';
|
|
69
182
|
import { urlToSpeech } from './url-speech.js';
|
|
70
183
|
import { textWithUrlToSpeech } from './url-speech.js';
|
|
71
|
-
export { matchesChoice, ANSWER_WORDS, CONFIRM_WORDS, isAnswer, isAgentRequest, isOtherRequest, isSpelledFragment, isAmountAnswer, amountInAnswer, pickChoice, readEmail, readPhone, readInternationalPhone, readIdentifier, last4, maskEmail, callerTextNumber, textDestination, callerIsInternational, normalizeAndFindCapture, anythingElse, goodbye, sayName, dateToWords, amountToSpeech, validateCardNumber, validateExpiration, matchInputs, checkOtp, maskDestination, validateOTP, OTP_EXPIRY_MS, validateDigits, validatePhone, validateEmail, urlToSpeech, textWithUrlToSpeech };
|
|
184
|
+
export { readMoneyAmount, isMoneyAmount, moneyIntentAllows, validateMoneyOptions, takeChoiceOffer, matchesChoice, ANSWER_WORDS, CONFIRM_WORDS, isAnswer, isAgentRequest, isOtherRequest, isSpelledFragment, isAmountAnswer, amountInAnswer, pickChoice, readEmail, readPhone, readInternationalPhone, readIdentifier, last4, maskEmail, callerTextNumber, textDestination, callerIsInternational, normalizeAndFindCapture, anythingElse, goodbye, sayName, dateToWords, amountToSpeech, validateCardNumber, validateExpiration, consumeActionReceipt, authorizeFormActionRetry, formSignature, selectFormAction, confirmFormRevision, prepareFormAction, actionConfirmationValid, beginFormAction, finishFormAction, actionGateVerified, sourceNeedsLoad, beginSourceRefresh, completeSourceRefresh, pickProviderQuestion, matchInputs, readFormAnswer, readInputProfile, resolveFormInputs, formInputApplies, formInputFilled, nextFormInput, formInputChain, openFormInstance, validateFormIncompleteOutcome, formIncompleteOutcome, validateFormExhaustion, exhaustFormInput, formInitialReadings, orderFormReadings, proposedFormValues, applyFormValidation, formReadingDependencies, exclusiveFormReadings, enrichFormMatches, expandFormActivations, settleFormActivations, rememberFormArguments, resetFormInputs, hasFormValue, formPath, formValuesEqual, formReadingReady, formReadingRelevant, formPreparation, queueFormReadings, pendingFormReadings, formWorkLimit, rejectFormReadings, invalidateFormInputs, acceptFormReadings, formInstanceApplies, closeFormInstance, checkOtp, maskDestination, validateOTP, OTP_EXPIRY_MS, validateDigits, validatePhone, validateEmail, urlToSpeech, textWithUrlToSpeech };
|
|
72
185
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/helpers/index.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/helpers/index.js"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;gCAkBmF,YAAY;8BAAZ,YAAY;kCAAZ,YAAY;qCAAZ,YAAY;gCAlB/D,mBAAmB;8BAiBrB,qBAAqB;6BAEkG,aAAa;8BAAb,aAAa;yBAAb,aAAa;+BAAb,aAAa;+BAAb,aAAa;kCAAb,aAAa;+BAAb,aAAa;+BAAb,aAAa;2BAAb,aAAa;0BACT,cAAc;0BAAd,cAAc;uCAAd,cAAc;+BAAd,cAAc;sBAAd,cAAc;0BAAd,cAAc;iCAAd,cAAc;gCAAd,cAAc;sCAAd,cAAc;wCAC/H,cAAc;6BACP,cAAc;wBAAd,cAAc;wBAAd,cAAc;4BACjC,iBAAiB;+BACd,uBAAuB;mCACC,kBAAkB;mCAAlB,kBAAkB;qCAxB+I,mBAAmB;yCAAnB,mBAAmB;8BAAnB,mBAAmB;iCAAnB,mBAAmB;oCAAnB,mBAAmB;kCAAnB,mBAAmB;wCAAnB,mBAAmB;gCAAnB,mBAAmB;iCAAnB,mBAAmB;mCAAnB,mBAAmB;gCAe1I,mBAAmB;mCAAnB,mBAAmB;sCAAnB,mBAAmB;qCAAnB,mBAAmB;4BAUtD,mBAAmB;+BAAnB,mBAAmB;iCAAnB,mBAAmB;kCAC6mB,iBAAiB;iCAAjB,iBAAiB;gCAAjB,iBAAiB;8BAAjB,iBAAiB;+BAAjB,iBAAiB;iCAAjB,iBAAiB;8CAAjB,iBAAiB;sCAAjB,iBAAiB;uCAAjB,iBAAiB;iCAAjB,iBAAiB;oCAAjB,iBAAiB;kCAAjB,iBAAiB;mCAAjB,iBAAiB;oCAAjB,iBAAiB;wCAAjB,iBAAiB;sCAAjB,iBAAiB;kCAAjB,iBAAiB;sCAAjB,iBAAiB;sCAAjB,iBAAiB;sCAAjB,iBAAiB;gCAAjB,iBAAiB;6BAAjB,iBAAiB;yBAAjB,iBAAiB;gCAAjB,iBAAiB;iCAAjB,iBAAiB;oCAAjB,iBAAiB;gCAAjB,iBAAiB;kCAAjB,iBAAiB;oCAAjB,iBAAiB;8BAAjB,iBAAiB;mCAAjB,iBAAiB;qCAAjB,iBAAiB;mCAAjB,iBAAiB;oCAAjB,iBAAiB;kCAAjB,iBAAiB;yBACzoB,gBAAgB;gCAAhB,gBAAgB;4BAAhB,gBAAgB;8BAAhB,gBAAgB;+BACzB,iBAAiB;8BAAjB,iBAAiB;8BAAjB,iBAAiB;4BAC7B,iBAAiB;oCAAjB,iBAAiB"}
|
package/dist/helpers/index.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { takeChoiceOffer } from './choice-offer.js';
|
|
2
|
+
import { consumeActionReceipt, authorizeFormActionRetry, formSignature, selectFormAction, confirmFormRevision, prepareFormAction, actionConfirmationValid, beginFormAction, finishFormAction, actionGateVerified } from './form-actions.js';
|
|
1
3
|
/**
|
|
2
4
|
* jsfe/helpers — the functions JSFE's system flows call from expressions, in ONE place for every host: reading an
|
|
3
5
|
* answer (matchesChoice, pickChoice, isAnswer, readEmail, readIdentifier, amountInAnswer, matchInputs …), saying
|
|
@@ -12,7 +14,9 @@
|
|
|
12
14
|
* Functions that need credentials or the network (sending a text or an email, geocoding, a store's API) belong to the
|
|
13
15
|
* host.
|
|
14
16
|
*/
|
|
17
|
+
import { sourceNeedsLoad, beginSourceRefresh, completeSourceRefresh, pickProviderQuestion } from './form-sources.js';
|
|
15
18
|
import { matchesChoice } from './matches-choice.js';
|
|
19
|
+
import { readMoneyAmount, isMoneyAmount, moneyIntentAllows, validateMoneyOptions } from './money.js';
|
|
16
20
|
import { ANSWER_WORDS, CONFIRM_WORDS, isAnswer, isAgentRequest, isOtherRequest, isSpelledFragment, isAmountAnswer, amountInAnswer, pickChoice } from './answer.js';
|
|
17
21
|
import { readEmail, readPhone, readInternationalPhone, readIdentifier, last4, maskEmail, callerTextNumber, textDestination, callerIsInternational } from './contact.js';
|
|
18
22
|
import { normalizeAndFindCapture } from './capture.js';
|
|
@@ -20,15 +24,16 @@ import { anythingElse, goodbye, sayName } from './phrases.js';
|
|
|
20
24
|
import { dateToWords } from './date-words.js';
|
|
21
25
|
import { amountToSpeech } from './amount-to-speech.js';
|
|
22
26
|
import { validateCardNumber, validateExpiration } from './card-checks.js';
|
|
23
|
-
import { matchInputs } from './match-inputs.js';
|
|
27
|
+
import { matchInputs, readFormAnswer, readInputProfile } from './match-inputs.js';
|
|
28
|
+
import { resolveFormInputs, formInputApplies, formInputFilled, nextFormInput, formInputChain, openFormInstance, validateFormIncompleteOutcome, formIncompleteOutcome, validateFormExhaustion, exhaustFormInput, formInitialReadings, orderFormReadings, proposedFormValues, applyFormValidation, formReadingDependencies, exclusiveFormReadings, enrichFormMatches, expandFormActivations, settleFormActivations, rememberFormArguments, resetFormInputs, hasFormValue, formPath, formValuesEqual, formReadingReady, formReadingRelevant, formPreparation, queueFormReadings, pendingFormReadings, formWorkLimit, rejectFormReadings, invalidateFormInputs, acceptFormReadings, formInstanceApplies, closeFormInstance } from './form-state.js';
|
|
24
29
|
import { checkOtp, maskDestination, validateOTP, OTP_EXPIRY_MS } from './otp-check.js';
|
|
25
30
|
import { validateDigits, validatePhone, validateEmail } from './validators.js';
|
|
26
31
|
import { urlToSpeech, textWithUrlToSpeech } from './url-speech.js';
|
|
27
|
-
export { matchesChoice, ANSWER_WORDS, CONFIRM_WORDS, isAnswer, isAgentRequest, isOtherRequest, isSpelledFragment, isAmountAnswer, amountInAnswer, pickChoice, readEmail, readPhone, readInternationalPhone, readIdentifier, last4, maskEmail, callerTextNumber, textDestination, callerIsInternational, normalizeAndFindCapture, anythingElse, goodbye, sayName, dateToWords, amountToSpeech, validateCardNumber, validateExpiration, matchInputs, checkOtp, maskDestination, validateOTP, OTP_EXPIRY_MS, validateDigits, validatePhone, validateEmail, urlToSpeech, textWithUrlToSpeech, };
|
|
32
|
+
export { readMoneyAmount, isMoneyAmount, moneyIntentAllows, validateMoneyOptions, takeChoiceOffer, matchesChoice, ANSWER_WORDS, CONFIRM_WORDS, isAnswer, isAgentRequest, isOtherRequest, isSpelledFragment, isAmountAnswer, amountInAnswer, pickChoice, readEmail, readPhone, readInternationalPhone, readIdentifier, last4, maskEmail, callerTextNumber, textDestination, callerIsInternational, normalizeAndFindCapture, anythingElse, goodbye, sayName, dateToWords, amountToSpeech, validateCardNumber, validateExpiration, consumeActionReceipt, authorizeFormActionRetry, formSignature, selectFormAction, confirmFormRevision, prepareFormAction, actionConfirmationValid, beginFormAction, finishFormAction, actionGateVerified, sourceNeedsLoad, beginSourceRefresh, completeSourceRefresh, pickProviderQuestion, matchInputs, readFormAnswer, readInputProfile, resolveFormInputs, formInputApplies, formInputFilled, nextFormInput, formInputChain, openFormInstance, validateFormIncompleteOutcome, formIncompleteOutcome, validateFormExhaustion, exhaustFormInput, formInitialReadings, orderFormReadings, proposedFormValues, applyFormValidation, formReadingDependencies, exclusiveFormReadings, enrichFormMatches, expandFormActivations, settleFormActivations, rememberFormArguments, resetFormInputs, hasFormValue, formPath, formValuesEqual, formReadingReady, formReadingRelevant, formPreparation, queueFormReadings, pendingFormReadings, formWorkLimit, rejectFormReadings, invalidateFormInputs, acceptFormReadings, formInstanceApplies, closeFormInstance, checkOtp, maskDestination, validateOTP, OTP_EXPIRY_MS, validateDigits, validatePhone, validateEmail, urlToSpeech, textWithUrlToSpeech, };
|
|
28
33
|
/** The functions flows call by name, ready to spread into a host's approved functions. */
|
|
29
34
|
export const STANDARD_FUNCTIONS = {
|
|
30
|
-
matchesChoice, isAnswer, isAgentRequest, isOtherRequest, isSpelledFragment, isAmountAnswer, amountInAnswer, pickChoice,
|
|
35
|
+
readMoneyAmount, isMoneyAmount, moneyIntentAllows, validateMoneyOptions, takeChoiceOffer, matchesChoice, isAnswer, isAgentRequest, isOtherRequest, isSpelledFragment, isAmountAnswer, amountInAnswer, pickChoice,
|
|
31
36
|
readEmail, readPhone, readInternationalPhone, readIdentifier, last4, maskEmail, callerTextNumber, textDestination, callerIsInternational,
|
|
32
|
-
normalizeAndFindCapture, anythingElse, goodbye, dateToWords, amountToSpeech, validateCardNumber, validateExpiration, matchInputs,
|
|
37
|
+
normalizeAndFindCapture, anythingElse, goodbye, sayName, dateToWords, amountToSpeech, validateCardNumber, validateExpiration, consumeActionReceipt, authorizeFormActionRetry, formSignature, selectFormAction, confirmFormRevision, prepareFormAction, actionConfirmationValid, beginFormAction, finishFormAction, actionGateVerified, sourceNeedsLoad, beginSourceRefresh, completeSourceRefresh, pickProviderQuestion, matchInputs, readFormAnswer, readInputProfile, resolveFormInputs, formInputApplies, formInputFilled, nextFormInput, formInputChain, openFormInstance, validateFormIncompleteOutcome, formIncompleteOutcome, validateFormExhaustion, exhaustFormInput, formInitialReadings, orderFormReadings, proposedFormValues, applyFormValidation, formReadingDependencies, exclusiveFormReadings, enrichFormMatches, expandFormActivations, settleFormActivations, rememberFormArguments, resetFormInputs, hasFormValue, formPath, formValuesEqual, formReadingReady, formReadingRelevant, formPreparation, queueFormReadings, pendingFormReadings, formWorkLimit, rejectFormReadings, invalidateFormInputs, acceptFormReadings, formInstanceApplies, closeFormInstance,
|
|
33
38
|
validateOTP, validateDigits, validatePhone, validateEmail, urlToSpeech, textWithUrlToSpeech,
|
|
34
39
|
};
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One input's reading of the answer: { value, evidence } or null. `evidence` is what the answer spent on it (digits
|
|
3
|
+
* or words), so two inputs that claim the same part of the answer are told apart from a compound answer.
|
|
4
|
+
*/
|
|
5
|
+
export function readInputProfile(profile: any, answer: any, isCurrent?: boolean): any;
|
|
1
6
|
/**
|
|
2
7
|
* candidates: [{ form, name, profile, value?, confirm_against? }] — the inputs of the form chain that apply now (`when`
|
|
3
8
|
* holds): the current form's first, then its parent's, and so on up. `value` is what is already stored for it.
|
|
@@ -19,7 +24,16 @@
|
|
|
19
24
|
* hits — what the answer fills, each { form, name, value, known } (`known`: equals the stored value);
|
|
20
25
|
* ambiguous — the readings to ask about (the first such group), or empty.
|
|
21
26
|
*/
|
|
27
|
+
/** Explicit information questions are a separate answer kind, so their topic is not accepted as a field value.
|
|
28
|
+
* A question clause is consumed as a question; independently separated clauses remain available for collection. */
|
|
29
|
+
export function readFormAnswer(answer: any, candidates: any, current: any, choiceKey: any, questions?: any[]): {
|
|
30
|
+
hits: any[];
|
|
31
|
+
ambiguous: any;
|
|
32
|
+
remaining: any[];
|
|
33
|
+
questions: any[];
|
|
34
|
+
};
|
|
22
35
|
export function matchInputs(answer: any, candidates: any, current: any, choiceKey: any): {
|
|
36
|
+
remaining?: any[] | undefined;
|
|
23
37
|
hits: {
|
|
24
38
|
[x: string]: any;
|
|
25
39
|
}[];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"match-inputs.d.ts","sourceRoot":"","sources":["../../src/helpers/match-inputs.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"match-inputs.d.ts","sourceRoot":"","sources":["../../src/helpers/match-inputs.js"],"names":[],"mappings":"AA2EA;;;GAGG;AACH,sFAmGC;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH;mHACmH;AACnH;;;;;EAeC;AACD;;;;;;EA6EC"}
|
|
@@ -11,6 +11,9 @@
|
|
|
11
11
|
import { matchesChoice } from './matches-choice.js';
|
|
12
12
|
import { pickChoice, amountInAnswer, isAmountAnswer } from './answer.js';
|
|
13
13
|
import { readEmail, readPhone, readInternationalPhone, readIdentifier } from './contact.js';
|
|
14
|
+
import { readSpokenMoney } from './spoken-money.js';
|
|
15
|
+
import { readMoneyAmount, moneyIntentAllows } from './money.js';
|
|
16
|
+
import { pickProviderQuestion } from './form-sources.js';
|
|
14
17
|
/** The answer as said: jsfe stores user input escaped (don\'t). */
|
|
15
18
|
function unescape(answer) {
|
|
16
19
|
return String(answer ?? '').replace(/\\(['"\\])/g, '$1');
|
|
@@ -19,32 +22,116 @@ function unescape(answer) {
|
|
|
19
22
|
function digitRuns(raw) {
|
|
20
23
|
return (raw.match(/\d(?:[\s.-]?\d)*/g) || []).map((r) => r.replace(/\D/g, ''));
|
|
21
24
|
}
|
|
25
|
+
/** Source intervals let different readers recognize the same evidence without agreeing on its type. */
|
|
26
|
+
function numberSpans(raw, value, amount = false) {
|
|
27
|
+
const runs = [...raw.matchAll(/\d(?:[\s.,-]?\d)*/g)];
|
|
28
|
+
if (!amount) {
|
|
29
|
+
const wanted = String(value).replace(/\D/g, '');
|
|
30
|
+
return runs.filter(m => { const d = m[0].replace(/\D/g, ''); return d === wanted || d === '1' + wanted; })
|
|
31
|
+
.map(m => [m.index, m.index + m[0].length]);
|
|
32
|
+
}
|
|
33
|
+
const spans = [];
|
|
34
|
+
for (let i = 0; i < runs.length; i++) {
|
|
35
|
+
for (let j = i; j < runs.length; j++) {
|
|
36
|
+
const a = runs[i].index, b = runs[j].index + runs[j][0].length;
|
|
37
|
+
// Include adjacent currency words for cents and dollars-and-cents readings.
|
|
38
|
+
const tail = (raw.slice(b).match(/^\s*(?:cents?|centavos?|dollars?|d[oó]lares?|pesos?|bucks?)\b/i) || [''])[0];
|
|
39
|
+
if (amountInAnswer(raw.slice(a, b) + tail) === Number(value)) {
|
|
40
|
+
spans.push([a, b + tail.length]);
|
|
41
|
+
break;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return spans.filter(([a, b]) => !spans.some(([c, d]) => c >= a && d <= b && (c > a || d < b)));
|
|
46
|
+
}
|
|
47
|
+
function choiceSpans(raw, words) {
|
|
48
|
+
const tokens = [...raw.matchAll(/[\p{L}\p{N}*#]+/gu)];
|
|
49
|
+
const spans = [];
|
|
50
|
+
for (const word of words) {
|
|
51
|
+
const n = (String(word).match(/[\p{L}\p{N}*#]+/gu) || []).length;
|
|
52
|
+
for (let i = 0; n && i + n <= tokens.length; i++) {
|
|
53
|
+
const a = tokens[i].index, b = tokens[i + n - 1].index + tokens[i + n - 1][0].length;
|
|
54
|
+
if (matchesChoice(raw.slice(a, b), [word]))
|
|
55
|
+
spans.push([a, b]);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return spans;
|
|
59
|
+
}
|
|
60
|
+
function emailSpans(raw, value) {
|
|
61
|
+
const literal = raw.toLowerCase().indexOf(value);
|
|
62
|
+
if (literal >= 0)
|
|
63
|
+
return [[literal, literal + value.length]];
|
|
64
|
+
// Spoken addresses may use at / arroba / dot / punto. Select the shortest source window yielding this address.
|
|
65
|
+
const tokens = [...raw.matchAll(/\S+/g)];
|
|
66
|
+
if (tokens.length > 128)
|
|
67
|
+
return [[0, raw.length]];
|
|
68
|
+
for (let size = 1; size <= tokens.length; size++) {
|
|
69
|
+
const spans = [];
|
|
70
|
+
for (let i = 0; i + size <= tokens.length; i++) {
|
|
71
|
+
const a = tokens[i].index, b = tokens[i + size - 1].index + tokens[i + size - 1][0].length;
|
|
72
|
+
if (readEmail(raw.slice(a, b)) === value)
|
|
73
|
+
spans.push([a, b]);
|
|
74
|
+
}
|
|
75
|
+
if (spans.length)
|
|
76
|
+
return spans;
|
|
77
|
+
}
|
|
78
|
+
return [[0, raw.length]];
|
|
79
|
+
}
|
|
22
80
|
/**
|
|
23
81
|
* One input's reading of the answer: { value, evidence } or null. `evidence` is what the answer spent on it (digits
|
|
24
82
|
* or words), so two inputs that claim the same part of the answer are told apart from a compound answer.
|
|
25
83
|
*/
|
|
26
|
-
function
|
|
84
|
+
export function readInputProfile(profile, answer, isCurrent = true) {
|
|
85
|
+
const raw = unescape(answer);
|
|
27
86
|
const kind = profile && profile.kind;
|
|
28
87
|
if (kind === 'choice') {
|
|
29
|
-
const choices = Array.isArray(profile.choices) ? profile.choices : []
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
88
|
+
const choices = (Array.isArray(profile.choices) ? profile.choices : []).filter(c => isCurrent || c.current_only !== true).map(c => ({ ...c,
|
|
89
|
+
numericWords: (c.words || []).filter(word => /^\d{2,}$/.test(String(word).trim())),
|
|
90
|
+
words: (c.words || []).concat(isCurrent ? c.keys || [] : []).filter(word => !(isAmountAnswer(raw) && /^\d+$/.test(String(word).trim())) && (isCurrent || profile.numeric_values === true || !/^[\d*#]$/.test(String(word).trim()))) }));
|
|
91
|
+
// Semantic numeric values may arrive in ASR groups ("95 52"), unlike menu keys.
|
|
92
|
+
// Keep choice policy in pickChoice and source evidence in the original utterance.
|
|
93
|
+
const runs = digitRuns(raw), joined = raw.replace(/\d(?:[\s.-]?\d)*/g, run => ' ' + run.replace(/\D/g, '') + ' ');
|
|
94
|
+
const numericSpans = c => {
|
|
95
|
+
if (isAmountAnswer(raw))
|
|
96
|
+
return [];
|
|
97
|
+
const words = c.numericWords.filter(word => runs.includes(String(word).trim()));
|
|
98
|
+
if (!words.length || pickChoice(joined, [{ ...c, words }], false) !== c.key)
|
|
99
|
+
return [];
|
|
100
|
+
return words.flatMap(word => numberSpans(raw, String(word).trim()).filter(([a, b]) => raw.slice(a, b).replace(/\D/g, '') === String(word).trim()));
|
|
101
|
+
};
|
|
102
|
+
const base = pickChoice(raw, choices, false);
|
|
103
|
+
const negative = c => ['no', 'decline', 'done', 'cancel'].includes(c.key) || c.negative === true;
|
|
104
|
+
const picked = choices.find(c => c.key === base);
|
|
105
|
+
const pool = picked && negative(picked) ? [picked] : choices.filter(c => !negative(c));
|
|
106
|
+
let readings = pool.filter(c => pickChoice(raw, [c], false) === c.key || numericSpans(c).length).map(c => {
|
|
107
|
+
const words = (c.words || []).filter(w => matchesChoice(raw, [w]));
|
|
108
|
+
const spans = [...choiceSpans(raw, words), ...numericSpans(c)].filter((span, index, all) => all.findIndex(other => other[0] === span[0] && other[1] === span[1]) === index);
|
|
109
|
+
return { value: c.key, ...(isCurrent && (c.keys || []).length && !isAmountAnswer(raw) && pickChoice(raw, [{ key: c.key, words: c.keys, exact: true }], false) === c.key ? { keypad: true } : {}), spans: spans.length ? spans : [[0, raw.length]], option_label: c.label || (c.words || []).find(w => !/^\d+$/.test(w)) || c.key, option_label_es: c.label_es || c.label || c.key };
|
|
110
|
+
});
|
|
111
|
+
// A more specific declared phrase can cover a shorter phrase of the same field's other option.
|
|
112
|
+
readings = readings.filter(r => !readings.some(other => other !== r && r.spans.every(([a, b]) => other.spans.some(([c, d]) => c <= a && d >= b && (c < a || d > b)))));
|
|
113
|
+
if (readings.length > 1)
|
|
114
|
+
return { alternatives: readings };
|
|
115
|
+
if (readings.length === 1) {
|
|
116
|
+
const { option_label, option_label_es, ...one } = readings[0];
|
|
117
|
+
return one;
|
|
39
118
|
}
|
|
119
|
+
return isCurrent && profile.accept_other === true && raw.trim() ? { value: raw.trim(), spans: [[0, raw.length]] } : null;
|
|
40
120
|
}
|
|
41
121
|
else if (kind === 'amount') {
|
|
42
|
-
const
|
|
122
|
+
const money = profile.money;
|
|
123
|
+
const read = money ? text => readMoneyAmount(text, money) : amountInAnswer;
|
|
124
|
+
const refused = !!money && money.intent === 'payment' && !moneyIntentAllows(raw, 'payment');
|
|
125
|
+
// A refusal is an uncommittable validation candidate, never a numeric correction. A declared
|
|
126
|
+
// validator may route it to recovery; the common batch always withholds acceptance.
|
|
127
|
+
if (refused)
|
|
128
|
+
return Number.isFinite(readMoneyAmount(raw, { ...money, intent: undefined })) ? { value: raw, refused: true, spans: [[0, raw.length]], evidence: ['money-intent'] } : null;
|
|
129
|
+
const n = (profile.marked === 'out_of_order' ? !isCurrent : profile.marked) ? (isAmountAnswer(raw) ? read(raw) : NaN) : read(raw);
|
|
43
130
|
const inRange = !isNaN(n) && (profile.min === undefined || n >= Number(profile.min)) && (profile.max === undefined || n <= Number(profile.max));
|
|
44
131
|
if (inRange) {
|
|
45
132
|
// the digits it was read from: the runs that start with its whole-dollar part ("82.50" said as "82 50" → 8250)
|
|
46
133
|
const runs = digitRuns(raw).filter((d) => d.indexOf(String(Math.trunc(n))) === 0);
|
|
47
|
-
return { value: String(n), evidence: runs.length ? runs.map((d) => `d:${d}`) : ['amount'] };
|
|
134
|
+
return { value: String(n), spans: [...numberSpans(raw, n, true), ...(money?.cardinals ? readSpokenMoney(raw).filter(reading => reading.value === n).map(reading => [reading.start, reading.end]) : [])], evidence: runs.length ? runs.map((d) => `d:${d}`) : ['amount'] };
|
|
48
135
|
}
|
|
49
136
|
else {
|
|
50
137
|
return null;
|
|
@@ -53,7 +140,7 @@ function readProfile(profile, raw, isCurrent) {
|
|
|
53
140
|
else if (kind === 'email') {
|
|
54
141
|
const e = readEmail(raw);
|
|
55
142
|
if (e) {
|
|
56
|
-
return { value: e, evidence: [`e:${e}`] };
|
|
143
|
+
return { value: e, spans: emailSpans(raw, e), evidence: [`e:${e}`] };
|
|
57
144
|
}
|
|
58
145
|
else {
|
|
59
146
|
return null;
|
|
@@ -62,7 +149,7 @@ function readProfile(profile, raw, isCurrent) {
|
|
|
62
149
|
else if (kind === 'phone') {
|
|
63
150
|
const p = readPhone(raw) || readInternationalPhone(raw);
|
|
64
151
|
if (p) {
|
|
65
|
-
return { value: p, evidence: [`d:${String(p).replace(/\D/g, '')}`] };
|
|
152
|
+
return { value: p, spans: numberSpans(raw, p), evidence: [`d:${String(p).replace(/\D/g, '')}`] };
|
|
66
153
|
}
|
|
67
154
|
else {
|
|
68
155
|
return null;
|
|
@@ -71,7 +158,7 @@ function readProfile(profile, raw, isCurrent) {
|
|
|
71
158
|
else if (kind === 'identifier') {
|
|
72
159
|
const id = readIdentifier(raw, profile.pattern || '');
|
|
73
160
|
if (id && id.kind === 'account') {
|
|
74
|
-
return { value: id.value, evidence: [`d:${id.value}`] };
|
|
161
|
+
return { value: id.value, spans: numberSpans(raw, id.value), evidence: [`d:${id.value}`] };
|
|
75
162
|
}
|
|
76
163
|
else {
|
|
77
164
|
return null;
|
|
@@ -82,7 +169,7 @@ function readProfile(profile, raw, isCurrent) {
|
|
|
82
169
|
const max = Number(profile.max || min);
|
|
83
170
|
const fits = [...new Set(digitRuns(raw).filter((d) => d.length >= min && d.length <= max))];
|
|
84
171
|
if (fits.length === 1) {
|
|
85
|
-
return { value: fits[0], evidence: [`d:${fits[0]}`] };
|
|
172
|
+
return { value: fits[0], spans: numberSpans(raw, fits[0]), evidence: [`d:${fits[0]}`] };
|
|
86
173
|
}
|
|
87
174
|
else {
|
|
88
175
|
return null; // none, or two different numbers of that length: not this input's answer by shape alone
|
|
@@ -98,16 +185,16 @@ function readProfile(profile, raw, isCurrent) {
|
|
|
98
185
|
}
|
|
99
186
|
if (m) {
|
|
100
187
|
const v = m[profile.group || 0] || m[0];
|
|
101
|
-
return { value: v, evidence: [`p:${String(v).toLowerCase()}`] };
|
|
188
|
+
return { value: v, spans: [[m.index + m[0].indexOf(v), m.index + m[0].indexOf(v) + v.length]], evidence: [`p:${String(v).toLowerCase()}`] };
|
|
102
189
|
}
|
|
103
190
|
else {
|
|
104
|
-
return null;
|
|
191
|
+
return isCurrent && profile.accept_other === true && raw.trim() ? { value: raw.trim(), fallback: true, spans: [[0, raw.length]] } : null;
|
|
105
192
|
}
|
|
106
193
|
}
|
|
107
194
|
else if (kind === 'text') {
|
|
108
195
|
const t = raw.trim();
|
|
109
196
|
if (isCurrent && t) {
|
|
110
|
-
return { value: t, evidence: ['text'] };
|
|
197
|
+
return { value: t, fallback: true, spans: [[0, raw.length]], evidence: ['text'] };
|
|
111
198
|
}
|
|
112
199
|
else {
|
|
113
200
|
return null; // free text fills only the input that was asked, or every answer would fit it
|
|
@@ -138,44 +225,62 @@ function readProfile(profile, raw, isCurrent) {
|
|
|
138
225
|
* hits — what the answer fills, each { form, name, value, known } (`known`: equals the stored value);
|
|
139
226
|
* ambiguous — the readings to ask about (the first such group), or empty.
|
|
140
227
|
*/
|
|
228
|
+
/** Explicit information questions are a separate answer kind, so their topic is not accepted as a field value.
|
|
229
|
+
* A question clause is consumed as a question; independently separated clauses remain available for collection. */
|
|
230
|
+
export function readFormAnswer(answer, candidates, current, choiceKey, questions = []) {
|
|
231
|
+
const raw = unescape(answer), parts = raw.split(/([?;\n]|\b(?:and|y)\b)/i), asked = [];
|
|
232
|
+
let consumed = false;
|
|
233
|
+
const remaining = parts.map(part => {
|
|
234
|
+
const text = part.trim();
|
|
235
|
+
if (!/^[¿\s]*(?:how\b|what\b|when\b|where\b|which\b|why\b|who\b|cu[aá]nt[oa]s?\b|qu[eé]\b|cu[aá]ndo\b|d[oó]nde\b|c[oó]mo\b|(?:can|could) you (?:tell|show)\b|(?:i (?:want|would like) to know|tell me|show me|quiero saber|quisiera saber|me gustar[ií]a saber|d[ií]game|dime|mu[eé]strame)\b)/i.test(text))
|
|
236
|
+
return part;
|
|
237
|
+
const key = pickProviderQuestion(text, questions);
|
|
238
|
+
if (!key || key.startsWith('__'))
|
|
239
|
+
return part;
|
|
240
|
+
consumed = true;
|
|
241
|
+
asked.push(key);
|
|
242
|
+
return ' '.repeat(part.length);
|
|
243
|
+
}).join('');
|
|
244
|
+
// Delimiters left by a consumed question are not a free-text field answer.
|
|
245
|
+
const fieldText = consumed && !/[a-záéíóúüñ0-9]/i.test(remaining.replace(/\b(?:and|y)\b/gi, '')) ? '' : remaining;
|
|
246
|
+
const matches = matchInputs(consumed ? fieldText : raw, candidates, current, consumed ? '' : choiceKey);
|
|
247
|
+
const provenance = h => ({ ...h, raw, utterance: raw });
|
|
248
|
+
return { ...matches, hits: matches.hits.map(provenance), ambiguous: matches.ambiguous.map(provenance), remaining: (matches.remaining || []).map(group => group.map(provenance)), questions: [...new Set(asked)] };
|
|
249
|
+
}
|
|
141
250
|
export function matchInputs(answer, candidates, current, choiceKey) {
|
|
142
251
|
const raw = unescape(answer);
|
|
143
252
|
const list = Array.isArray(candidates) ? candidates.filter((c) => c && c.name && c.profile) : [];
|
|
144
253
|
const cur = current && typeof current === 'object' ? current : {};
|
|
145
254
|
const isCur = (c) => (Array.isArray(cur.names) ? cur.names.indexOf(c.name) > -1 : c.name === cur.name) && (cur.form === undefined || c.form === cur.form);
|
|
146
255
|
const own = Array.isArray(cur.names) ? undefined : list.filter(isCur)[0]; // a group has no single own choice list
|
|
147
|
-
// A declared confirm_against pair with the current input is read in full below, even when its own choice was picked.
|
|
148
|
-
const pairedWithOwn = !!own && list.some((c) => c !== own && ((Array.isArray(c.confirm_against) && c.confirm_against.indexOf(own.name) > -1) || (Array.isArray(own.confirm_against) && own.confirm_against.indexOf(c.name) > -1)));
|
|
149
|
-
if (own && choiceKey && !pairedWithOwn && own.profile.kind === 'choice' && (own.profile.choices || []).some((c) => c && c.key === choiceKey)) {
|
|
150
|
-
return { hits: [{ form: own.form, name: own.name, value: choiceKey, known: own.value === choiceKey }], ambiguous: [] };
|
|
151
|
-
}
|
|
152
|
-
else {
|
|
153
|
-
// no direct pick of the current input's own choice: read every candidate's shape
|
|
154
|
-
}
|
|
155
256
|
// A form's scope level: 0 for the current input's form, then each further form in the order the chain lists them.
|
|
156
257
|
const forms = [];
|
|
157
258
|
if (own) {
|
|
158
|
-
forms.push(own.form);
|
|
259
|
+
forms.push(own.scope || own.form);
|
|
159
260
|
}
|
|
160
261
|
else {
|
|
161
262
|
// the current input is not among the candidates (it no longer applies): levels follow the list order
|
|
162
263
|
}
|
|
163
|
-
list.forEach((c) => { if (forms.indexOf(c.form) === -1) {
|
|
164
|
-
forms.push(c.form);
|
|
264
|
+
list.forEach((c) => { if (forms.indexOf(c.scope || c.form) === -1) {
|
|
265
|
+
forms.push(c.scope || c.form);
|
|
165
266
|
}
|
|
166
267
|
else { /* already ranked */ } });
|
|
167
268
|
const readings = [];
|
|
168
269
|
for (const c of list) {
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
270
|
+
let r = readInputProfile(c.profile, raw, isCur(c));
|
|
271
|
+
// A semantic choice supplied by ask-choice (e.g. spoken keypad keys) is a reading, not an early return.
|
|
272
|
+
if (!r && c === own && choiceKey && c.profile.kind === 'choice' && (c.profile.choices || []).some(x => x.key === choiceKey)) {
|
|
273
|
+
r = { ...(r || {}), value: choiceKey, spans: r?.spans?.length ? r.spans : [[0, raw.length]] };
|
|
172
274
|
}
|
|
173
|
-
|
|
174
|
-
|
|
275
|
+
if (r) {
|
|
276
|
+
for (const option of r.alternatives || [r]) {
|
|
277
|
+
readings.push({ form: c.form, name: c.name, value: option.value, ...(option.refused === true ? { refused: true } : {}), known: c.value !== undefined && c.value !== null && String(c.value) === String(option.value), spans: option.spans, keypad: option.keypad === true, fallback: option.fallback === true,
|
|
278
|
+
...(r.alternatives ? { option_label: option.option_label, option_label_es: option.option_label_es } : {}), group: c.group, exclusive: c.exclusive === true, level: forms.indexOf(c.scope || c.form), current: isCur(c), confirm: Array.isArray(c.confirm_against) ? c.confirm_against : [] });
|
|
279
|
+
}
|
|
175
280
|
}
|
|
176
281
|
}
|
|
177
282
|
// Readings that spent the same evidence form a group (connected components).
|
|
178
|
-
const conflicts = (a, b) => a.
|
|
283
|
+
const conflicts = (a, b) => a.form === b.form && (a.name === b.name && a.value !== b.value || a.exclusive && b.exclusive && a.group && a.group === b.group && a.name !== b.name) || (a.spans || []).some(([s, e]) => (b.spans || []).some(([t, u]) => s < u && t < e));
|
|
179
284
|
const parent = readings.map((_, i) => i);
|
|
180
285
|
const find = (i) => (parent[i] === i ? i : (parent[i] = find(parent[i])));
|
|
181
286
|
for (let i = 0; i < readings.length; i++) {
|
|
@@ -190,15 +295,23 @@ export function matchInputs(answer, candidates, current, choiceKey) {
|
|
|
190
295
|
}
|
|
191
296
|
const groups = {};
|
|
192
297
|
readings.forEach((r, i) => { (groups[find(i)] = groups[find(i)] || []).push(r); });
|
|
193
|
-
const strip = ({ evidence, level, current: c, confirm, ...r }) => r;
|
|
298
|
+
const strip = ({ evidence, spans, keypad, fallback, level, group, exclusive, current: c, confirm, ...r }) => r;
|
|
194
299
|
const paired = (a, b) => a.confirm.indexOf(b.name) > -1 || b.confirm.indexOf(a.name) > -1;
|
|
195
300
|
const hits = [];
|
|
196
301
|
let ambiguous = [];
|
|
197
|
-
|
|
302
|
+
const remaining = [];
|
|
303
|
+
for (const group of Object.values(groups)) {
|
|
304
|
+
// Free text is a fallback; a specific claim to the same source evidence is stronger.
|
|
305
|
+
const specific = group.filter(m => !m.fallback);
|
|
306
|
+
const members = specific.length ? specific.concat(group.filter(m => m.fallback && specific.some(s => paired(m, s)))) : group;
|
|
198
307
|
let winners;
|
|
199
308
|
if (members.length === 1) {
|
|
200
309
|
winners = members;
|
|
201
310
|
}
|
|
311
|
+
else if (members.some((m) => m.current && m.keypad)) {
|
|
312
|
+
// Explicit menu keys select their current field, including a current any_of group.
|
|
313
|
+
winners = members.filter((m) => m.current && m.keypad);
|
|
314
|
+
}
|
|
202
315
|
else if (members.some((m) => m.current)) {
|
|
203
316
|
winners = members.filter((m) => m.current);
|
|
204
317
|
}
|
|
@@ -216,8 +329,8 @@ export function matchInputs(answer, candidates, current, choiceKey) {
|
|
|
216
329
|
ambiguous = asked.map(strip);
|
|
217
330
|
}
|
|
218
331
|
else {
|
|
219
|
-
|
|
332
|
+
remaining.push(asked.map(strip));
|
|
220
333
|
}
|
|
221
334
|
}
|
|
222
|
-
return { hits, ambiguous };
|
|
335
|
+
return { hits, ambiguous, ...(remaining.length ? { remaining } : {}) };
|
|
223
336
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** A profile must declare its currency; currently only USD grammar is supported. No exchange-rate inference. */
|
|
2
|
+
export function validateMoneyOptions(options: any): any;
|
|
3
|
+
/** One declared intent acceptance rule, shared by parsing and domain validation. Uncertainty is not refusal. */
|
|
4
|
+
export function moneyIntentAllows(input: any, intent: any): boolean;
|
|
5
|
+
/** Number or NaN. currency is required; cardinals defaults false and intent is optional.
|
|
6
|
+
* Explicit foreign currencies, negative amounts and independent unequal readings fail closed.
|
|
7
|
+
* Refusal is rejected only under the explicitly declared payment intent; it never grants consent.
|
|
8
|
+
*/
|
|
9
|
+
export function readMoneyAmount(input: any, options: any): any;
|
|
10
|
+
/** True exactly when the opted-in reader returns a finite amount (bare digits included). */
|
|
11
|
+
export function isMoneyAmount(input: any, options: any): boolean;
|
|
12
|
+
//# sourceMappingURL=money.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"money.d.ts","sourceRoot":"","sources":["../../src/helpers/money.js"],"names":[],"mappings":"AAIA,gHAAgH;AAChH,wDAGC;AACD,gHAAgH;AAChH,oEAQC;AAWD;;;GAGG;AACH,+DAYC;AACD,4FAA4F;AAC5F,iEAAsG"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/** Opt-in currency grammar. Existing amountInAnswer/isAmountAnswer retain their published 1.6 semantics. */
|
|
2
|
+
import { numericAmountReading } from './answer.js';
|
|
3
|
+
import { scanSpokenMoney } from './spoken-money.js';
|
|
4
|
+
/** A profile must declare its currency; currently only USD grammar is supported. No exchange-rate inference. */
|
|
5
|
+
export function validateMoneyOptions(options) {
|
|
6
|
+
if (!options || typeof options !== 'object' || Array.isArray(options) || Object.keys(options).some(key => !['currency', 'cardinals', 'intent'].includes(key)) || options.currency !== 'USD' || options.cardinals !== undefined && typeof options.cardinals !== 'boolean' || options.intent !== undefined && options.intent !== 'payment')
|
|
7
|
+
throw new Error('Money options require currency USD, optional boolean cardinals and optional payment intent');
|
|
8
|
+
return options;
|
|
9
|
+
}
|
|
10
|
+
/** One declared intent acceptance rule, shared by parsing and domain validation. Uncertainty is not refusal. */
|
|
11
|
+
export function moneyIntentAllows(input, intent) {
|
|
12
|
+
if (intent === undefined || intent === null || intent === '')
|
|
13
|
+
return true;
|
|
14
|
+
if (intent !== 'payment')
|
|
15
|
+
throw new Error('Unsupported money intent');
|
|
16
|
+
const raw = String(input ?? '').replace(/\\(['"\\])/g, '$1').toLowerCase().replace(/[’‘]/g, "'");
|
|
17
|
+
const refused = /\b(?:can't|cannot|can not|won't|will not|don't|do not|refuse to)\s+(?:want\s+to\s+)?pay(?:\b|(?=\d))|\bno\s+(?:lo\s+)?(?:se\s+)?(?:puede|pudo|puedo|quiero|voy\s+a)\s+pagar(?:\b|(?=\d))/.test(raw);
|
|
18
|
+
const unable = /\b(?:unable|not able)\s+to\s+pay(?:\b|(?=\d))|\bno\s+(?:soy|somos|es|son)\s+capaz(?:es)?\s+de\s+pagar(?:\b|(?=\d))|\bincapaz\s+de\s+pagar(?:\b|(?=\d))|\b(?:no\s+(?:me\s+)?es\s+posible|me\s+es\s+imposible|no\s+podr[eé])\s+pagar(?:\b|(?=\d))/.test(raw);
|
|
19
|
+
const deferred = /\b(?:pay|paying|pagar|abonar)(?:\b|(?=\d))/.test(raw) && /\b(?:later|tomorrow|next week|not today|despu[eé]s|m[aá]s tarde|ma[nñ]ana|hasta)\b/.test(raw);
|
|
20
|
+
return !refused && !unable && !deferred;
|
|
21
|
+
}
|
|
22
|
+
function unsupportedCurrency(raw) {
|
|
23
|
+
return /(?:^|[^a-záéíóúüñ])(?:pesos?|mxn|euros?|eur|pounds?|gbp|yen|jpy|cad|aud|nzd|hkd|sgd|chf|canadian|canadienses?|australian|australianos?)(?![a-záéíóúüñ])|[€£¥₱₹₩₽]|\bmx\s*\$/i.test(raw);
|
|
24
|
+
}
|
|
25
|
+
function negativeAmount(raw) {
|
|
26
|
+
return /\b(?:minus|negative|menos|negativo)\b|(?:^|[\s,(;:])[-−]\s*\$?\s*(?:\d|[a-záéíóúüñ])/i.test(raw);
|
|
27
|
+
}
|
|
28
|
+
/** Explicit identifier labels cannot become amounts merely because their number words precede a currency. */
|
|
29
|
+
function introducedAsIdentifier(raw, start) {
|
|
30
|
+
return /\b(?:sub[ -]?accounts?|accounts?|subcuentas?|cuentas?|cards?(?:\s+number)?|tarjetas?|ending(?:\s+in)?|terminad[ao]s?\s+en|que\s+termina\s+en|termina\s+en|codes?|c[oó]digos?|zip(?:\s+code)?|postal|n[uú]mero|number)\s*(?:de\s+|is\s+|es\s+)?$/i.test(raw.slice(0, start));
|
|
31
|
+
}
|
|
32
|
+
/** Number or NaN. currency is required; cardinals defaults false and intent is optional.
|
|
33
|
+
* Explicit foreign currencies, negative amounts and independent unequal readings fail closed.
|
|
34
|
+
* Refusal is rejected only under the explicitly declared payment intent; it never grants consent.
|
|
35
|
+
*/
|
|
36
|
+
export function readMoneyAmount(input, options) {
|
|
37
|
+
validateMoneyOptions(options);
|
|
38
|
+
const raw = String(input ?? '').replace(/\\(['"\\])/g, '$1');
|
|
39
|
+
if (unsupportedCurrency(raw) || negativeAmount(raw) || !moneyIntentAllows(raw, options.intent))
|
|
40
|
+
return NaN;
|
|
41
|
+
const { readings, invalid } = scanSpokenMoney(raw);
|
|
42
|
+
if (invalid.length || options.cardinals !== true && readings.length || readings.some(reading => introducedAsIdentifier(raw, reading.start)))
|
|
43
|
+
return NaN;
|
|
44
|
+
const numeric = readings.reduce((text, reading) => text.slice(0, reading.start) + ' '.repeat(reading.end - reading.start) + text.slice(reading.end), raw);
|
|
45
|
+
const remaining = numericAmountReading(numeric);
|
|
46
|
+
if (!readings.length)
|
|
47
|
+
return remaining.value;
|
|
48
|
+
if (remaining.candidates && Number.isNaN(remaining.value))
|
|
49
|
+
return NaN;
|
|
50
|
+
const values = readings.map(reading => reading.value).concat(Number.isNaN(remaining.value) ? [] : [remaining.value]);
|
|
51
|
+
return new Set(values).size === 1 ? values[0] : NaN;
|
|
52
|
+
}
|
|
53
|
+
/** True exactly when the opted-in reader returns a finite amount (bare digits included). */
|
|
54
|
+
export function isMoneyAmount(input, options) { return Number.isFinite(readMoneyAmount(input, options)); }
|