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.
Files changed (103) hide show
  1. package/README.md +32 -4
  2. package/dist/forms/action-projection.d.ts +4 -0
  3. package/dist/forms/action-projection.d.ts.map +1 -0
  4. package/dist/forms/action-projection.js +21 -0
  5. package/dist/forms/action-recovery.d.ts +12 -0
  6. package/dist/forms/action-recovery.d.ts.map +1 -0
  7. package/dist/forms/action-recovery.js +125 -0
  8. package/dist/forms/action-tools.d.ts +29 -0
  9. package/dist/forms/action-tools.d.ts.map +1 -0
  10. package/dist/forms/action-tools.js +6 -0
  11. package/dist/forms/address-flows.d.ts +2 -0
  12. package/dist/forms/address-flows.d.ts.map +1 -0
  13. package/dist/forms/address-flows.js +39 -0
  14. package/dist/forms/choice-flow.d.ts +419 -0
  15. package/dist/forms/choice-flow.d.ts.map +1 -0
  16. package/dist/forms/choice-flow.js +707 -0
  17. package/dist/forms/contracts.d.ts +237 -0
  18. package/dist/forms/contracts.d.ts.map +1 -0
  19. package/dist/forms/contracts.js +278 -0
  20. package/dist/forms/flows.d.ts +611 -0
  21. package/dist/forms/flows.d.ts.map +1 -0
  22. package/dist/forms/flows.js +675 -0
  23. package/dist/forms/identity-flows.d.ts +17 -0
  24. package/dist/forms/identity-flows.d.ts.map +1 -0
  25. package/dist/forms/identity-flows.js +302 -0
  26. package/dist/forms/index.d.ts +7 -0
  27. package/dist/forms/index.d.ts.map +1 -0
  28. package/dist/forms/index.js +10 -0
  29. package/dist/forms/prompt-compiler.d.ts +3 -0
  30. package/dist/forms/prompt-compiler.d.ts.map +1 -0
  31. package/dist/forms/prompt-compiler.js +30 -0
  32. package/dist/forms/session-actions.d.ts +14 -0
  33. package/dist/forms/session-actions.d.ts.map +1 -0
  34. package/dist/forms/session-actions.js +40 -0
  35. package/dist/helpers/address.d.ts +27 -0
  36. package/dist/helpers/address.d.ts.map +1 -0
  37. package/dist/helpers/address.js +29 -0
  38. package/dist/helpers/answer.d.ts +5 -0
  39. package/dist/helpers/answer.d.ts.map +1 -1
  40. package/dist/helpers/answer.js +5 -3
  41. package/dist/helpers/choice-offer.d.ts +3 -0
  42. package/dist/helpers/choice-offer.d.ts.map +1 -0
  43. package/dist/helpers/choice-offer.js +6 -0
  44. package/dist/helpers/form-actions.d.ts +13 -0
  45. package/dist/helpers/form-actions.d.ts.map +1 -0
  46. package/dist/helpers/form-actions.js +144 -0
  47. package/dist/helpers/form-sources.d.ts +7 -0
  48. package/dist/helpers/form-sources.d.ts.map +1 -0
  49. package/dist/helpers/form-sources.js +47 -0
  50. package/dist/helpers/form-state.d.ts +94 -0
  51. package/dist/helpers/form-state.d.ts.map +1 -0
  52. package/dist/helpers/form-state.js +870 -0
  53. package/dist/helpers/index.d.ts +114 -1
  54. package/dist/helpers/index.d.ts.map +1 -1
  55. package/dist/helpers/index.js +9 -4
  56. package/dist/helpers/match-inputs.d.ts +14 -0
  57. package/dist/helpers/match-inputs.d.ts.map +1 -1
  58. package/dist/helpers/match-inputs.js +154 -41
  59. package/dist/helpers/money.d.ts +12 -0
  60. package/dist/helpers/money.d.ts.map +1 -0
  61. package/dist/helpers/money.js +54 -0
  62. package/dist/helpers/spoken-money.d.ts +20 -0
  63. package/dist/helpers/spoken-money.d.ts.map +1 -0
  64. package/dist/helpers/spoken-money.js +143 -0
  65. package/dist/host/action-journal.d.ts +98 -0
  66. package/dist/host/action-journal.d.ts.map +1 -0
  67. package/dist/host/action-journal.js +218 -0
  68. package/dist/host/action-session.d.ts +12 -0
  69. package/dist/host/action-session.d.ts.map +1 -0
  70. package/dist/host/action-session.js +29 -0
  71. package/dist/host/capabilities.d.ts +14 -0
  72. package/dist/host/capabilities.d.ts.map +1 -0
  73. package/dist/host/capabilities.js +60 -0
  74. package/dist/host/checkpoint-codec-error.d.ts +6 -0
  75. package/dist/host/checkpoint-codec-error.d.ts.map +1 -0
  76. package/dist/host/checkpoint-codec-error.js +4 -0
  77. package/dist/host/index.d.ts +6 -0
  78. package/dist/host/index.d.ts.map +1 -1
  79. package/dist/host/index.js +6 -0
  80. package/dist/host/intent-stages.d.ts +1 -0
  81. package/dist/host/intent-stages.d.ts.map +1 -1
  82. package/dist/host/intent-stages.js +3 -3
  83. package/dist/host/node.d.ts +17 -0
  84. package/dist/host/node.d.ts.map +1 -0
  85. package/dist/host/node.js +64 -0
  86. package/dist/host/recovery-identity.d.ts +16 -0
  87. package/dist/host/recovery-identity.d.ts.map +1 -0
  88. package/dist/host/recovery-identity.js +73 -0
  89. package/dist/host/recovery-policy.d.ts +5 -0
  90. package/dist/host/recovery-policy.d.ts.map +1 -0
  91. package/dist/host/recovery-policy.js +54 -0
  92. package/dist/host/runtime-manifest.d.ts +3 -0
  93. package/dist/host/runtime-manifest.d.ts.map +1 -0
  94. package/dist/host/runtime-manifest.js +38 -0
  95. package/dist/index.d.ts +61 -0
  96. package/dist/index.d.ts.map +1 -1
  97. package/dist/index.js +186 -45
  98. package/dist/runtime-identity.d.ts +3 -0
  99. package/dist/runtime-identity.d.ts.map +1 -0
  100. package/dist/runtime-identity.js +1 -0
  101. package/docs/development-release.md +25 -0
  102. package/docs/forms.md +132 -0
  103. package/package.json +15 -4
@@ -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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;8BAc8B,qBAAqB;6BACkG,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;4BAC7C,mBAAmB;yBACuB,gBAAgB;gCAAhB,gBAAgB;4BAAhB,gBAAgB;8BAAhB,gBAAgB;+BACzB,iBAAiB;8BAAjB,iBAAiB;8BAAjB,iBAAiB;4BAC7B,iBAAiB;oCAAjB,iBAAiB"}
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"}
@@ -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":"AA0GA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH;;;;;EAuEC"}
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 readProfile(profile, raw, isCurrent) {
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
- const key = pickChoice(raw, choices, false);
31
- if (key && key.indexOf('__') !== 0) {
32
- const choice = choices.filter((c) => c && c.key === key)[0] || { words: [] };
33
- const words = (choice.words || []).filter((w) => matchesChoice(raw, [w]));
34
- // a number word is the same evidence as a number read by another input ('62' as a sub-account and as $62)
35
- return { value: key, evidence: words.map((w) => (/^[\d\s.-]+$/.test(String(w)) ? `d:${String(w).replace(/\D/g, '')}` : `w:${String(w).toLowerCase()}`)) };
36
- }
37
- else {
38
- return null; // no choice, or an AGENT / EXIT request (ask-choice owns those)
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 n = profile.marked ? (isAmountAnswer(raw) ? amountInAnswer(raw) : NaN) : amountInAnswer(raw);
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
- const r = readProfile(c.profile, raw, isCur(c));
170
- if (r) {
171
- readings.push({ form: c.form, name: c.name, value: r.value, known: c.value !== undefined && c.value !== null && String(c.value) === String(r.value), evidence: r.evidence, level: forms.indexOf(c.form), current: isCur(c), confirm: Array.isArray(c.confirm_against) ? c.confirm_against : [] });
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
- else {
174
- // this input's shape is not in the answer
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.evidence.some((e) => b.evidence.indexOf(e) > -1);
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
- for (const members of Object.values(groups)) {
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
- // a second ambiguous group in one answer is left for the next question
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)); }