jsfe 1.32.0 → 1.34.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"address-flows.d.ts","sourceRoot":"","sources":["../../src/forms/address-flows.js"],"names":[],"mappings":"AA4CA,0BAAmF"}
1
+ {"version":3,"file":"address-flows.d.ts","sourceRoot":"","sources":["../../src/forms/address-flows.js"],"names":[],"mappings":"AA6CA,0BAAmF"}
@@ -14,8 +14,8 @@ const address = defineForm({ id: 'collect-geocoded-address', name: 'CollectGeoco
14
14
  inputs: [{ name: 'zip', field: 'zip', argument_description: '5-digit ZIP code', names: ['zip', 'zip code', 'postal code', 'código postal', 'codigo postal'], validator: 'address-zip-validate', prepare: 'address-zip-prompt', question_from: 'zip_question', question: 'What is the 5-digit zip code of your new address?', question_es: '¿Cuál es el código postal de 5 dígitos de su nueva dirección?' },
15
15
  { name: 'city', argument_description: 'city name alone', label: 'city', label_es: 'ciudad', names: ['city', 'ciudad'], profile: { kind: 'choice', choices: [], accept_other: true }, choices_from: 'city_choices', when: { 'form.data.city_known': true }, depends_on: ['zip'], reset_on: ['zip'], prepare: 'address-city-prompt', question_from: 'city_question', question: 'Which city is it?', question_es: '¿Qué ciudad es?' },
16
16
  { name: 'city_state', argument_description: 'city and state', label: 'city and state', label_es: 'ciudad y estado', names: ['city', 'ciudad'], profile: { kind: 'text' }, when: { 'form.data.need_city': true }, depends_on: ['zip'], question: 'What city and state is that in?', question_es: '¿En qué ciudad y estado está?' },
17
- { name: 'street', argument_description: 'street number and street name, without the apartment', label: 'street address', label_es: 'dirección', names: ['street', 'calle'], profile: { kind: 'pattern', regex: streetPattern, accept_other: true }, depends_on: ['zip'], validator: 'address-street-validate', prepare: 'address-street-prompt', question_from: 'street_question', question: 'What is the street address?', question_es: '¿Cuál es la dirección?' },
18
- { name: 'unit', argument_description: 'apartment or unit number alone, such as 4 or 12B', reset_on: ['street'], label: 'apartment or unit', label_es: 'apartamento o unidad', names: ['apartment', 'apt', 'unit', 'apartamento', 'unidad', 'departamento'], profile: { kind: 'pattern', regex: '(?:\\bapartment|\\bapartamento|\\bdepartamento|\\bapt\\.?|\\bunit|\\bunidad|#)\\s*([\\w-]+)', group: 1, accept_other: true }, question: 'Is there an apartment or unit number? Type the number, or NONE.', question_es: '¿Hay un número de apartamento o unidad? Escriba el número, o NINGUNO.' }],
17
+ { name: 'street', argument_description: 'street number and street name, without the apartment', label: 'street address', label_es: 'dirección', names: ['street', 'calle'], profile: { kind: 'pattern', regex: streetPattern, spoken_digits: 'house_number', accept_other: true }, depends_on: ['zip'], validator: 'address-street-validate', prepare: 'address-street-prompt', question_from: 'street_question', question: 'What is the street address?', question_es: '¿Cuál es la dirección?' },
18
+ { name: 'unit', argument_description: 'apartment or unit number alone, such as 4 or 12B', reset_on: ['street'], label: 'apartment or unit', label_es: 'apartamento o unidad', names: ['apartment', 'apt', 'unit', 'apartamento', 'unidad', 'departamento'], profile: { kind: 'pattern', regex: '(?:\\bapartment|\\bapartamento|\\bdepartamento|\\bapt\\.?|\\bunit|\\bunidad|#)\\s*([\\w-]+)', group: 1, spoken_digits: true, accept_other: true }, question: 'Is there an apartment or unit number? Type the number, or NONE.', question_es: '¿Hay un número de apartamento o unidad? Escriba el número, o NINGUNO.' }],
19
19
  before_confirm: [S('address_resolved', 'address_result', E(resolvedAddress, 'cargo.forms[form_instance].values', 'cargo.forms[form_instance].data'))],
20
20
  confirm: { named_fields: true, reply_context: 'This is a mailing address read back for confirmation. A reply may correct one part or several at once: the street number, street name or street type, the apartment or unit, the city, or the ZIP code.', no_keys: ['2', 'not right', 'not correct', 'incorrect', 'wrong', 'no es correcto', 'no es correcta', 'incorrecta'], question: 'Your new address is {{address_result.line}}. Is that correct?', question_es: 'Su nueva dirección es {{address_result.line}}. ¿Es correcta?' },
21
21
  after: [S('address_result_store', 'noop', '(cargo.forms[form_instance].values.resolved = address_result, cargo.address = address_result, cargo.addressDraft = null, true)')], declined: [S('address_retry_noop', 'noop', 'true')] });
@@ -25,7 +25,8 @@ const zip = C('address-zip-validate', 'Read city and state from geocoder evidenc
25
25
  S('address_zip_result', 'noop', "(cargo.forms[form].data=Object.assign({},cargo.forms[form].data,{location:location,need_city:!location,city_known:!!location,city_choice:!!(location&&location.cities.length),city_choices:" + E(cityChoices, 'location') + "}), location&&!location.cities.length&&cargo.forms[form].values.city===undefined?(cargo.forms[form].values.city=location.city,cargo.forms[form].accepted.city={origin:'derived',revision:cargo.forms[form].revision||0}):null, cargo.inputCheck={ok:true,value:String(value)}, true)")
26
26
  ], { parameters: [P('form'), P('value')], variables: V({ components: '', result: null, location: null }), contract: { tools: ['geocode-zip'], result: { path: 'cargo.inputCheck', type: 'object' } } });
27
27
  const street = C('address-street-validate', 'Validate a proposed street against its ZIP; retain unverified wording when geocoding is inconclusive.', [
28
- W('address_street_malformed', "!/^\\d{1,6}\\s+[A-Za-z\\xC0-\\xFF]/.test(String(value))", S('address_street_invalid', 'noop', "(cargo.inputCheck={ok:false,message:'Please give the street number and street name.',message_es:'Indique el número y nombre de la calle.'},true)")), W('address_street_stop', "!/^\\d{1,6}\\s+[A-Za-z\\xC0-\\xFF]/.test(String(value))", { id: 'address_street_invalid_end', type: 'END' }),
28
+ S('address_street_said', 'value', "joinSpokenDigits(String(value),'house_number')"),
29
+ W('address_street_malformed', "!/^\\d{1,6}\\s+(?:\\d{1,3}\\s+)?[A-Za-z\\xC0-\\xFF]/.test(String(value))", S('address_street_invalid', 'noop', "(cargo.inputCheck={ok:false,message:'Please give the street number and street name.',message_es:'Indique el número y nombre de la calle.'},true)")), W('address_street_stop', "!/^\\d{1,6}\\s+(?:\\d{1,3}\\s+)?[A-Za-z\\xC0-\\xFF]/.test(String(value))", { id: 'address_street_invalid_end', type: 'END' }),
29
30
  S('address_street_components', 'components', "'postal_code:'+values.zip+'|country:US'"), { id: 'address_street_tool', type: 'CALL-TOOL', tool: 'geocode-street', variable: 'result', args: { address: '{{value}}', components: '{{components}}' }, onFail: S('address_street_failed', 'result', 'null') },
30
31
  S('address_street_projection', 'location', E(streetLocation, 'result', 'value', 'values.zip')),
31
32
  S('address_street_result', 'noop', "(cargo.forms[form].data.street=location, location.city&&cargo.forms[form].accepted&&cargo.forms[form].accepted.city&&cargo.forms[form].accepted.city.origin==='derived'&&cargo.forms[form].values.city!==location.city?(cargo.forms[form].values.city=location.city):null, cargo.inputCheck={ok:true,value:String(value)}, true)")
@@ -1 +1 @@
1
- {"version":3,"file":"identity-flows.d.ts","sourceRoot":"","sources":["../../src/forms/identity-flows.js"],"names":[],"mappings":"AA+BA,+GAA+G;AAC/G,oFAOC;AAQD,wFAAwF;AACxF,iDAGC;AACD,6CAMC;AACD,qHAQC;AAMD,4EAKC;AA6FD,2DAOC;AACD,4DAOC;AACD,gHAAgH;AAChH,8DAGC;AAoJD,8EAGC;AA4BD,4FAA4F;AAC5F;;;;EAOC;AAWD,oGAOC;AA/PD,iCAAgC,wIAAwI,CAAC;AACzK,wCAAuC,CAAC,CAAC;AA2QzC,0BAAmJ"}
1
+ {"version":3,"file":"identity-flows.d.ts","sourceRoot":"","sources":["../../src/forms/identity-flows.js"],"names":[],"mappings":"AA+BA,+GAA+G;AAC/G,oFAOC;AAQD,wFAAwF;AACxF,iDAGC;AACD,6CAMC;AACD,qHAQC;AAMD,4EAKC;AA6FD,2DAOC;AACD,4DAOC;AACD,gHAAgH;AAChH,8DAGC;AAqJD,8EAGC;AA4BD,4FAA4F;AAC5F;;;;EAOC;AAWD,oGAOC;AAhQD,iCAAgC,wIAAwI,CAAC;AACzK,wCAAuC,CAAC,CAAC;AA4QzC,0BAAmJ"}
@@ -150,8 +150,8 @@ const contact = worker('get-cell-or-email', 'GetCellOrEmail', 'Identify a suppli
150
150
  const declined = worker('contact-caller-declined', 'ContactCallerDeclined', 'Repeat contact collection without offering caller ID.', { parameters: [param('contact_options', 'Current contact service options', 'object')], steps: [set('contact_declined_options', 'contact_options', "Object.assign({},contact_options,{caller_on_file:'no',intro:'',intro_es:''})"), contactReplay('contact_declined_repeat')] });
151
151
  const splitFailed = worker('contact-split-failed', 'ContactSplitFailed', 'Restart a split identifier after too many digits.', { parameters: [param('contact_options', 'Current contact service options', 'object')], steps: [set('contact_split_options', 'contact_options', "Object.assign({},contact_options,{caller_on_file:'no',intro:cargo.voice?'I could not put that number together. Please enter it on your keypad.':'I could not put that number together. Please type the whole number.',intro_es:cargo.voice?'No pude juntar ese número. Por favor ingréselo con el teclado.':'No pude juntar ese número. Por favor escriba el número completo.'})"), contactReplay('contact_split_repeat')] });
152
152
  const rejected = worker('contact-not-on-file', 'ContactNotOnFile', 'Re-collect a rejected account contact, then offer the common retry menu at the rejection limit.', { parameters: [param('contact_options', 'Current contact service options', 'object'), param('error_message', 'Rejection sentence'), param('error_message_es', 'Spanish rejection sentence')], steps: [set('contact_rejected_options', 'contact_options', "Object.assign({},contact_options,{caller_on_file:cargo.contactKind === 'caller' || contact_options.caller_on_file === 'no' ? 'no':'',fromMenu:false,intro:error_message,intro_es:error_message_es})"), when('contact_rejected_again', 'cargo.gceRejects <= 2', contactReplay('contact_rejected_collect', { gce_rejected: true })), set('contact_rejected_error', 'contact_error', 'error_message'), set('contact_rejected_error_es', 'contact_error_es', 'error_message_es'), contactMenu('contact_rejected_menu')] });
153
- const AUTH_DEFAULTS = { retry_flow: '', cancel_flow: '', cell_number: '', email: '', email_validator: '', cell_validator: '', code_validator: '', code_confirm: '', answer_handler: '', retry_returns: false, retrying: false, declined_caller: false, parent_form: '', task: '', task_es: '', failed_flow: '', contact_valid: false, auth_result: false, auth_contact_error: '', auth_contact_error_es: '', auth_previous_options: null, auth_restart: false, auth_join: false, otp_expiry_ms: OTP_EXPIRY_MS };
154
- const authKeys = ['retry_flow', 'cancel_flow', 'email_validator', 'cell_validator', 'code_validator', 'code_confirm', 'answer_handler', 'retry_returns', 'parent_form', 'task', 'task_es', 'failed_flow'];
153
+ const AUTH_DEFAULTS = { retry_flow: '', cancel_flow: '', cell_number: '', email: '', email_validator: '', cell_validator: '', code_validator: '', code_confirm: '', other_whole_answer: false, answer_handler: '', retry_returns: false, retrying: false, declined_caller: false, parent_form: '', task: '', task_es: '', failed_flow: '', contact_valid: false, auth_result: false, auth_contact_error: '', auth_contact_error_es: '', auth_previous_options: null, auth_restart: false, auth_join: false, otp_expiry_ms: OTP_EXPIRY_MS };
154
+ const authKeys = ['retry_flow', 'cancel_flow', 'email_validator', 'cell_validator', 'code_validator', 'code_confirm', 'other_whole_answer', 'answer_handler', 'retry_returns', 'parent_form', 'task', 'task_es', 'failed_flow'];
155
155
  // One destination's sends, by phone (last 10 digits) or email: a caller gets 3 codes per contact (owner 2026-11-01).
156
156
  export const OTP_DESTINATION_JS = "(cargo.otp_cell_number ? String(cargo.otp_cell_number).replace(/[^0-9]/g,'').slice(-10) : String(cargo.otp_email || '').toLowerCase())";
157
157
  export const MAX_OTP_SENDS_PER_CONTACT = 3;
@@ -253,7 +253,7 @@ function otpPhoneUnavailable(value) {
253
253
  }
254
254
  // What the reply ranker may take at the code question beside the code: the step's offers. Email and another account are
255
255
  // offered only where the step offers them (a code sent by text; a caller-ID preselection).
256
- const OTP_REPLY_CHOICES = [{ key: 'email', label: 'use the email on the account instead of the phone' }, { key: 'another_account', label: 'use another account' },
256
+ const OTP_REPLY_CHOICES = [{ key: 'email', label: 'use the email on the account instead of the phone' }, { key: 'another_account', label: 'use another account' }, { key: 'no_other_account', label: 'says they have no other account, this is their only one' },
257
257
  { key: 'where', label: 'asks where the code was sent' }, { key: 'cannot_find', label: 'cannot find the code, or it is not there' }, { key: 'another_way', label: 'asks for another way to get the code or to verify' }, { key: 'resend', label: 'the code did not arrive; send it again' }, { key: 'no_phone', label: 'no longer has that phone or number' }, { key: '__agent', label: 'speak with a person (a live agent)' }];
258
258
  const validateCode = worker('validate-otp-code', 'ValidateOtpCode', 'Validate a challenge response with explicit bounded retries, alternate-contact handling and tenant callbacks.', { loopBound: '3 mismatches, 3 format misses, 2 resends, 3 sends per contact', variables: vars({ otp_phrases: OTP_WORDS, email_too: false, no_code_given: false, wants_other_account: false, other_account_given: false, otp_validation_result: false, whatsapp_invite_sent: false, invite_text: '', otp_cancel: 'contact-support', answer_key: '', answer_value: '', otp_reply_choices: OTP_REPLY_CHOICES, otp_offers: [], otp_reply: {}, otp_where: '', otp_where_es: '' }), contract: { role: 'gate', summary: 'Validate the active one-time challenge', tools: ['validate-otp', 'send-whatsapp-invite'], result: { path: 'cargo.otpVerified', type: 'boolean' } }, steps: [
259
259
  set('otp_cancel', 'otp_cancel', "cargo.authCancelFlow || 'contact-support'"),
@@ -261,7 +261,7 @@ const validateCode = worker('validate-otp-code', 'ValidateOtpCode', 'Validate a
261
261
  set('otp_reset_no_code', 'no_code_given', 'false'), set('otp_reset_invite', 'whatsapp_invite_sent', 'false'), set('otp_reset_callback', 'answer_key', "''"),
262
262
  when('otp_tenant_code', 'cargo.authOptions && cargo.authOptions.code_validator && normalized_otp_code.length !== 6 && /[0-9]/.test(user_choice)', call('otp_tenant_code_call', '{{cargo.authOptions.code_validator}}', { answer: '{{user_choice}}' })),
263
263
  set('otp_other_given', 'other_account_given', "answer_key === 'another_account' ? (cargo.authOtherAccount=String(answer_value || ''),true) : false"),
264
- set('otp_other_requested', 'wants_other_account', 'other_account_given || !!(cargo.callerPreselected && normalized_otp_code.length !== 6 && (matchesChoice(user_choice,otp_phrases.other) || otp_phrases.other_alone.includes(user_choice.replace(/[.,!?¡¿]/g,\'\').trim())))'),
264
+ set('otp_other_requested', 'wants_other_account', 'other_account_given || !!(cargo.callerPreselected && normalized_otp_code.length !== 6 && ((cargo.authOptions && cargo.authOptions.other_whole_answer ? pickChoice(user_choice,[{key:\'other\',phrases:otp_phrases.other,exact:true}],false) === \'other\' : matchesChoice(user_choice,otp_phrases.other)) || otp_phrases.other_alone.includes(user_choice.replace(/[.,!?¡¿]/g,\'\').trim())))'),
265
265
  when('otp_tenant_answer', 'cargo.authOptions && cargo.authOptions.answer_handler && normalized_otp_code.length !== 6 && !wants_other_account && answer_key !== \'code_notice\'', call('otp_tenant_answer_call', '{{cargo.authOptions.answer_handler}}', { answer: '{{user_choice}}' })),
266
266
  set('otp_where_early', 'otp_where', expression(otpWhere, 'cargo')), set('otp_where_early_es', 'otp_where_es', expression(otpWhere, 'cargo', 'true')),
267
267
  cases('otp_response', {
@@ -294,7 +294,7 @@ const validateCode = worker('validate-otp-code', 'ValidateOtpCode', 'Validate a
294
294
  // states why they called is told again how to give the code, never sent to intent detection. Then the set's declared exits
295
295
  // ($data.gate_exits['authenticate-user'], read as $gateExits: what needs no code, e.g. a payment link), which leave as a
296
296
  // new request.
297
- set('otp_offers', 'otp_offers', "otp_reply_choices.filter(function(c){return (c.key !== 'email' || !!cargo.otp_cell_number) && (c.key !== 'another_account' || !!cargo.callerPreselected);})"
297
+ set('otp_offers', 'otp_offers', "otp_reply_choices.filter(function(c){return (c.key !== 'email' || !!cargo.otp_cell_number) && (c.key !== 'another_account' || !!cargo.callerPreselected) && (c.key !== 'no_other_account' || !!(cargo.callerPreselected && cargo.authOptions && cargo.authOptions.other_whole_answer));})"
298
298
  + ".concat(((typeof $callers !== 'undefined' && $callers && $callers['authenticate-user']) || []).map(function(c){return {key:'usecase:'+c.id,label:'states the reason for the call: '+c.label};}))"
299
299
  + ".concat(((typeof $gateExits !== 'undefined' && $gateExits && $gateExits['authenticate-user']) || []).map(function(a){return {key:'exit:'+a.key,label:a.label,route:'new_request'};}))"),
300
300
  when('otp_off_topic', 'no_code_given', { id: 'otp_reply_rank', type: 'DISPATCH', onResponse: 'continue', responseContext: 'Please say the 6-digit verification code I sent you. (Por favor diga el código de verificación de 6 dígitos que le envié.)', responseChoices: 'otp_offers', responseVariable: 'otp_reply', onNoIntent: 'stay' }),
@@ -308,6 +308,7 @@ const validateCode = worker('validate-otp-code', 'ValidateOtpCode', 'Validate a
308
308
  "condition: !no_code_given && otp_reply && otp_reply.key === 'another_way'": say('otp_ranked_no_other_way', '{{otp_where}} To reach a person, {{cargo.verb}} AGENT.', '{{otp_where_es}} Para hablar con una persona, {{cargo.verb_es}} AGENTE.'),
309
309
  "condition: !no_code_given && otp_reply && otp_reply.key === 'resend'": call('otp_ranked_resend', 'resend-otp-code', { resend_reason: 'requested' }),
310
310
  "condition: !no_code_given && otp_reply && otp_reply.key === 'no_phone'": otpMenu('otp_ranked_no_phone', "No problem. Then the code can't reach you there. You can use the email on your account instead, or {{cargo.verb}} AGENT to reach a person.", 'No hay problema. Entonces el código no le puede llegar ahí. Puede usar el correo electrónico de su cuenta, o {{cargo.verb_es}} AGENTE para hablar con una persona.', false),
311
+ "condition: !no_code_given && otp_reply && otp_reply.key === 'no_other_account'": say('otp_ranked_no_other_account', 'I understand. We still need to secure access to your account.', 'Entiendo. Aún necesitamos asegurar el acceso a su cuenta.'),
311
312
  "condition: !no_code_given && otp_reply && otp_reply.key === '__agent'": call('otp_ranked_agent', 'live-agent-requested', undefined, 'reboot'),
312
313
  default: effect('otp_reply_route_none', 'true')
313
314
  }),
@@ -1,4 +1,3 @@
1
- /** Address interpretation and geocoder projection, independent of collection order. */
2
1
  export function addressDraft(raw: any): {
3
2
  zip: string;
4
3
  street: string;
@@ -1 +1 @@
1
- {"version":3,"file":"address.d.ts","sourceRoot":"","sources":["../../src/helpers/address.js"],"names":[],"mappings":"AAAA,uFAAuF;AACvF;;;;;EAQC;AACD;;;;;SASC;AACD,gDAEC;AACD;;;;;EASC;AACD;;;;;;;;EAIC"}
1
+ {"version":3,"file":"address.d.ts","sourceRoot":"","sources":["../../src/helpers/address.js"],"names":[],"mappings":"AAEA;;;;;EAQC;AACD;;;;;SASC;AACD,gDAEC;AACD;;;;;EASC;AACD;;;;;;;;EAIC"}
@@ -1,6 +1,7 @@
1
1
  /** Address interpretation and geocoder projection, independent of collection order. */
2
+ import { joinSpokenDigits } from './spoken-digits.js';
2
3
  export function addressDraft(raw) {
3
- const said = String(raw || '').trim();
4
+ const said = joinSpokenDigits(String(raw || '').trim(), 'house_number');
4
5
  const zip = /(?:^|\D)(\d{5})(?:-\d{4})?(?:\D|$)/.exec(said)?.[1] || '';
5
6
  const unit = /(?:\bapt\.?|\bunit|#)\s*([\w-]+)/i.exec(said)?.[1] || '';
6
7
  let street = said.split(/[,\n]/)[0].replace(/\s*(?:apt\.?|unit|#)\s*[\w-]+.*$/i, '').trim();
@@ -1 +1 @@
1
- {"version":3,"file":"answer.d.ts","sourceRoot":"","sources":["../../src/helpers/answer.js"],"names":[],"mappings":"AAsNA;;;GAGG;AACH,wEAkBC;AAED,qHAAqH;AACrH,+CAGC;AAED;;;;GAIG;AACH,mDAIC;AAED,+HAA+H;AAC/H,oDAIC;AAcD,2GAA2G;AAC3G,uDAGC;AAOD;;;;;GAKG;AACH,+DAUC;AAKD;;;;GAIG;AACH,oDAEC;AAqBD;;;;;;GAMG;AACH,mDAAmF;AAEnF,kGAAkG;AAClG;;;EAoCC;AAqBD,oEAqBC;AASD,gJAAgJ;AAChJ,sEAGC;AAED;;;;;;;;;GASG;AACH,6EA4GC;;;;;;;;AA7fD,qCAKwE"}
1
+ {"version":3,"file":"answer.d.ts","sourceRoot":"","sources":["../../src/helpers/answer.js"],"names":[],"mappings":"AAsNA;;;GAGG;AACH,wEAkBC;AAED,qHAAqH;AACrH,+CAGC;AAED;;;;GAIG;AACH,mDAIC;AAED,+HAA+H;AAC/H,oDAIC;AAcD,2GAA2G;AAC3G,uDAGC;AAOD;;;;;GAKG;AACH,+DAUC;AAKD;;;;GAIG;AACH,oDAEC;AAuBD;;;;;;GAMG;AACH,mDAAmF;AAEnF,kGAAkG;AAClG;;;EAoCC;AAqBD,oEAqBC;AASD,gJAAgJ;AAChJ,sEAGC;AAED;;;;;;;;;GASG;AACH,6EA4GC;;;;;;;;AA/fD,qCAKwE"}
@@ -325,6 +325,8 @@ export function isAmountAnswer(input) {
325
325
  const IDENTIFIER_NUMBER = /\b(?:sub[\s-]?accounts?|accounts?|subcuentas?|cuentas?|cards?|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+)?\d[\d\s-]*/gi;
326
326
  // Digits spoken one at a time ("2 4 8 6"): a card ending or a code read back, never an amount.
327
327
  const SPELLED_DIGITS = /\b\d(?:\s+\d\b){2,}/g;
328
+ // The same digits with a money word after them are the amount: "1 5 0 dollars".
329
+ const SPELLED_DOLLARS = /\b\d(?:\s+\d\b)+(?=\s*(?:dollars?|d[oó]lar(?:es)?|bucks?)\b)/gi;
328
330
  // "10 con 18 centavos", "10 dollars and 18 cents", "$10 y 18 centavos"; speech-to-text also writes "76 con50 centavos", and
329
331
  // callers say "171 con un 6 centavos" (owner 2026-10-18; a prod call went to the guru): an "un" before the cents.
330
332
  const DOLLARS_AND_CENTS = /(\d+)\s*(?:dollars?|d[oó]lar(?:es)?|bucks?)?\s*(?:and|con|y)\s*(?:un\s+)?(\d{1,2})\s*(?:cents?|centavos?)\b/i;
@@ -367,7 +369,7 @@ export function numericAmountReading(input) {
367
369
  else {
368
370
  // no dollars-and-cents phrase
369
371
  }
370
- t = t.replace(IDENTIFIER_NUMBER, ' ').replace(SPELLED_DIGITS, ' ').replace(DOLLARS_JOINED_CENTS, (m, sign, d, word, joiner, c) => (sign || word || (CENTS_JOINERS.includes(joiner.toLowerCase()) && c.length === 2) ? ` $${d}.${c.padStart(2, '0')} ` : m))
372
+ t = t.replace(SPELLED_DOLLARS, (digits) => digits.replace(/\s+/g, '')).replace(IDENTIFIER_NUMBER, ' ').replace(SPELLED_DIGITS, ' ').replace(DOLLARS_JOINED_CENTS, (m, sign, d, word, joiner, c) => (sign || word || (CENTS_JOINERS.includes(joiner.toLowerCase()) && c.length === 2) ? ` $${d}.${c.padStart(2, '0')} ` : m))
371
373
  .replace(DOLLARS_SPACE_CENTS, (m, sign, d, c) => ` $${d}.${c} `);
372
374
  const found = [...t.matchAll(/(\$\s*)?(\d+(?:\.\d{1,2})?)(\s*(?:dollars?|d[oó]lar(?:es)?|bucks?|cents?|centavos?)\b)?/gi)]
373
375
  .map((m) => ({ v: parseFloat(m[2]), marked: !!(m[1] || m[3] || m[2].includes('.')), cents: !m[1] && /cent/i.test(m[3] || '') }));
@@ -123,6 +123,8 @@ export namespace STANDARD_FUNCTIONS {
123
123
  export { validateEmail };
124
124
  export { urlToSpeech };
125
125
  export { textWithUrlToSpeech };
126
+ export { joinSpokenDigits };
127
+ export { wholeSpokenNumber };
126
128
  }
127
129
  import { continuationSafetyProtocol } from '../forms/continuation-safety.js';
128
130
  import { cancelFormTask } from './form-conversation.js';
@@ -253,5 +255,8 @@ import { validatePhone } from './validators.js';
253
255
  import { validateEmail } from './validators.js';
254
256
  import { urlToSpeech } from './url-speech.js';
255
257
  import { textWithUrlToSpeech } from './url-speech.js';
256
- export { continuationSafetyProtocol, cancelFormTask, contextualResponseProtocol, validateFormResponse, hasFormResponse, formProposalPolicy, stageFormProposal, nextFormProposal, formProposalReadings, bindFormProposal, decideFormProposal, commitFormProposals, settleFormProposalValidation, formProposalProtocol, validateFormProposalPolicy, validateFormProposalDescriptor, formProposalDescriptor, clearFormMisses, formNoOwner, missFormInput, formMustComplete, formInputPrompt, formAcknowledges, consumeFormFeedback, formConversationOutcome, readMoneyAmount, isMoneyAmount, moneyIntentAllows, isPastPaymentStatement, isPartialMoneyAnswer, validateMoneyOptions, takeChoiceOffer, saysConfirmWord, confirmOutOfOrderLine, matchesChoice, ANSWER_WORDS, CONFIRM_WORDS, isAnswer, isAgentRequest, isExitRequest, isOtherRequest, isSpelledFragment, isAmountAnswer, amountInAnswer, pickChoice, withoutPromptEcho, readEmail, readPhone, readInternationalPhone, readIdentifier, last4, maskEmail, callerTextNumber, textDestination, callerIsInternational, normalizeAndFindCapture, anythingElse, goodbye, sayName, sorryNotCaught, dateToWords, amountToSpeech, validateCardNumber, validateExpiration, consumeActionReceipt, authorizeFormActionRetry, formSignature, selectFormAction, confirmFormRevision, prepareFormAction, actionConfirmationValid, beginFormAction, finishFormAction, actionGateVerified, sourceNeedsLoad, beginSourceRefresh, completeSourceRefresh, pickProviderQuestion, matchInputs, readFormAnswer, readInputProfile, formAcceptanceTrace, 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, answerResetsFormReplays, rejectFormReadings, invalidateFormInputs, acceptFormReadings, formInstanceApplies, formParentResume, closeFormInstance, checkOtp, clearOtp, maskDestination, validateOTP, OTP_EXPIRY_MS, validateDigits, validatePhone, validateEmail, urlToSpeech, textWithUrlToSpeech };
258
+ import { joinSpokenDigits } from './spoken-digits.js';
259
+ import { spokenDigitsText } from './spoken-digits.js';
260
+ import { wholeSpokenNumber } from './spoken-digits.js';
261
+ export { continuationSafetyProtocol, cancelFormTask, contextualResponseProtocol, validateFormResponse, hasFormResponse, formProposalPolicy, stageFormProposal, nextFormProposal, formProposalReadings, bindFormProposal, decideFormProposal, commitFormProposals, settleFormProposalValidation, formProposalProtocol, validateFormProposalPolicy, validateFormProposalDescriptor, formProposalDescriptor, clearFormMisses, formNoOwner, missFormInput, formMustComplete, formInputPrompt, formAcknowledges, consumeFormFeedback, formConversationOutcome, readMoneyAmount, isMoneyAmount, moneyIntentAllows, isPastPaymentStatement, isPartialMoneyAnswer, validateMoneyOptions, takeChoiceOffer, saysConfirmWord, confirmOutOfOrderLine, matchesChoice, ANSWER_WORDS, CONFIRM_WORDS, isAnswer, isAgentRequest, isExitRequest, isOtherRequest, isSpelledFragment, isAmountAnswer, amountInAnswer, pickChoice, withoutPromptEcho, readEmail, readPhone, readInternationalPhone, readIdentifier, last4, maskEmail, callerTextNumber, textDestination, callerIsInternational, normalizeAndFindCapture, anythingElse, goodbye, sayName, sorryNotCaught, dateToWords, amountToSpeech, validateCardNumber, validateExpiration, consumeActionReceipt, authorizeFormActionRetry, formSignature, selectFormAction, confirmFormRevision, prepareFormAction, actionConfirmationValid, beginFormAction, finishFormAction, actionGateVerified, sourceNeedsLoad, beginSourceRefresh, completeSourceRefresh, pickProviderQuestion, matchInputs, readFormAnswer, readInputProfile, formAcceptanceTrace, 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, answerResetsFormReplays, rejectFormReadings, invalidateFormInputs, acceptFormReadings, formInstanceApplies, formParentResume, closeFormInstance, checkOtp, clearOtp, maskDestination, validateOTP, OTP_EXPIRY_MS, validateDigits, validatePhone, validateEmail, urlToSpeech, textWithUrlToSpeech, joinSpokenDigits, spokenDigitsText, wholeSpokenNumber };
257
262
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/helpers/index.js"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;2CACyC,iCAAiC;+BAI2F,wBAAwB;2CAH/G,0BAA0B;qCAA1B,0BAA0B;gCAA1B,0BAA0B;mCACsE,qBAAqB;kCAArB,qBAAqB;iCAArB,qBAAqB;qCAArB,qBAAqB;iCAArB,qBAAqB;mCAArB,qBAAqB;oCAArB,qBAAqB;6CAArB,qBAAqB;qCAC/E,wBAAwB;2CAAxB,wBAAwB;+CAAxB,wBAAwB;uCAAxB,wBAAwB;gCACyB,wBAAwB;4BAAxB,wBAAwB;8BAAxB,wBAAwB;iCAAxB,wBAAwB;gCAAxB,wBAAwB;iCAAxB,wBAAwB;oCAAxB,wBAAwB;wCAAxB,wBAAwB;gCAmB9D,YAAY;8BAAZ,YAAY;kCAAZ,YAAY;uCAAZ,YAAY;qCAAZ,YAAY;qCAAZ,YAAY;gCAlB3G,mBAAmB;gCA0BwE,mBAAmB;sCAAnB,mBAAmB;8BAThH,qBAAqB;6BAEoI,aAAa;8BAAb,aAAa;yBAAb,aAAa;+BAAb,aAAa;8BAAb,aAAa;+BAAb,aAAa;kCAAb,aAAa;+BAAb,aAAa;+BAAb,aAAa;2BAAb,aAAa;kCAAb,aAAa;0BAC3C,cAAc;0BAAd,cAAc;uCAAd,cAAc;+BAAd,cAAc;sBAAd,cAAc;0BAAd,cAAc;iCAAd,cAAc;gCAAd,cAAc;sCAAd,cAAc;wCAC/H,cAAc;6BACS,cAAc;wBAAd,cAAc;wBAAd,cAAc;+BAAd,cAAc;4BACjD,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;4BAUO,mBAAmB;+BAAnB,mBAAmB;iCAAnB,mBAAmB;oCAAnB,mBAAmB;kCAC0lB,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;wCAAjB,iBAAiB;mCAAjB,iBAAiB;qCAAjB,iBAAiB;mCAAjB,iBAAiB;oCAAjB,iBAAiB;iCAAjB,iBAAiB;kCAAjB,iBAAiB;yBACzqB,gBAAgB;yBAAhB,gBAAgB;gCAAhB,gBAAgB;4BAAhB,gBAAgB;8BAAhB,gBAAgB;+BACnC,iBAAiB;8BAAjB,iBAAiB;8BAAjB,iBAAiB;4BAC7B,iBAAiB;oCAAjB,iBAAiB"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/helpers/index.js"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;2CACyC,iCAAiC;+BAI2F,wBAAwB;2CAH/G,0BAA0B;qCAA1B,0BAA0B;gCAA1B,0BAA0B;mCACsE,qBAAqB;kCAArB,qBAAqB;iCAArB,qBAAqB;qCAArB,qBAAqB;iCAArB,qBAAqB;mCAArB,qBAAqB;oCAArB,qBAAqB;6CAArB,qBAAqB;qCAC/E,wBAAwB;2CAAxB,wBAAwB;+CAAxB,wBAAwB;uCAAxB,wBAAwB;gCACyB,wBAAwB;4BAAxB,wBAAwB;8BAAxB,wBAAwB;iCAAxB,wBAAwB;gCAAxB,wBAAwB;iCAAxB,wBAAwB;oCAAxB,wBAAwB;wCAAxB,wBAAwB;gCAmB9D,YAAY;8BAAZ,YAAY;kCAAZ,YAAY;uCAAZ,YAAY;qCAAZ,YAAY;qCAAZ,YAAY;gCAlB3G,mBAAmB;gCA0BwE,mBAAmB;sCAAnB,mBAAmB;8BAThH,qBAAqB;6BAEoI,aAAa;8BAAb,aAAa;yBAAb,aAAa;+BAAb,aAAa;8BAAb,aAAa;+BAAb,aAAa;kCAAb,aAAa;+BAAb,aAAa;+BAAb,aAAa;2BAAb,aAAa;kCAAb,aAAa;0BAC3C,cAAc;0BAAd,cAAc;uCAAd,cAAc;+BAAd,cAAc;sBAAd,cAAc;0BAAd,cAAc;iCAAd,cAAc;gCAAd,cAAc;sCAAd,cAAc;wCAC/H,cAAc;6BACS,cAAc;wBAAd,cAAc;wBAAd,cAAc;+BAAd,cAAc;4BACjD,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;4BAUO,mBAAmB;+BAAnB,mBAAmB;iCAAnB,mBAAmB;oCAAnB,mBAAmB;kCAC0lB,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;wCAAjB,iBAAiB;mCAAjB,iBAAiB;qCAAjB,iBAAiB;mCAAjB,iBAAiB;oCAAjB,iBAAiB;iCAAjB,iBAAiB;kCAAjB,iBAAiB;yBACzqB,gBAAgB;yBAAhB,gBAAgB;gCAAhB,gBAAgB;4BAAhB,gBAAgB;8BAAhB,gBAAgB;+BACnC,iBAAiB;8BAAjB,iBAAiB;8BAAjB,iBAAiB;4BAE7B,iBAAiB;oCAAjB,iBAAiB;iCADI,oBAAoB;iCAApB,oBAAoB;kCAApB,oBAAoB"}
@@ -34,8 +34,9 @@ import { matchInputs, readFormAnswer, saysConfirmWord, confirmOutOfOrderLine, re
34
34
  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, answerResetsFormReplays, rejectFormReadings, invalidateFormInputs, acceptFormReadings, formInstanceApplies, formParentResume, closeFormInstance } from './form-state.js';
35
35
  import { checkOtp, clearOtp, maskDestination, validateOTP, OTP_EXPIRY_MS } from './otp-check.js';
36
36
  import { validateDigits, validatePhone, validateEmail } from './validators.js';
37
+ import { joinSpokenDigits, spokenDigitsText, wholeSpokenNumber } from './spoken-digits.js';
37
38
  import { urlToSpeech, textWithUrlToSpeech } from './url-speech.js';
38
- export { continuationSafetyProtocol, cancelFormTask, contextualResponseProtocol, validateFormResponse, hasFormResponse, formProposalPolicy, stageFormProposal, nextFormProposal, formProposalReadings, bindFormProposal, decideFormProposal, commitFormProposals, settleFormProposalValidation, formProposalProtocol, validateFormProposalPolicy, validateFormProposalDescriptor, formProposalDescriptor, clearFormMisses, formNoOwner, missFormInput, formMustComplete, formInputPrompt, formAcknowledges, consumeFormFeedback, formConversationOutcome, readMoneyAmount, isMoneyAmount, moneyIntentAllows, isPastPaymentStatement, isPartialMoneyAnswer, validateMoneyOptions, takeChoiceOffer, saysConfirmWord, confirmOutOfOrderLine, matchesChoice, ANSWER_WORDS, CONFIRM_WORDS, isAnswer, isAgentRequest, isExitRequest, isOtherRequest, isSpelledFragment, isAmountAnswer, amountInAnswer, pickChoice, withoutPromptEcho, readEmail, readPhone, readInternationalPhone, readIdentifier, last4, maskEmail, callerTextNumber, textDestination, callerIsInternational, normalizeAndFindCapture, anythingElse, goodbye, sayName, sorryNotCaught, dateToWords, amountToSpeech, validateCardNumber, validateExpiration, consumeActionReceipt, authorizeFormActionRetry, formSignature, selectFormAction, confirmFormRevision, prepareFormAction, actionConfirmationValid, beginFormAction, finishFormAction, actionGateVerified, sourceNeedsLoad, beginSourceRefresh, completeSourceRefresh, pickProviderQuestion, matchInputs, readFormAnswer, readInputProfile, formAcceptanceTrace, 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, answerResetsFormReplays, rejectFormReadings, invalidateFormInputs, acceptFormReadings, formInstanceApplies, formParentResume, closeFormInstance, checkOtp, clearOtp, maskDestination, validateOTP, OTP_EXPIRY_MS, validateDigits, validatePhone, validateEmail, urlToSpeech, textWithUrlToSpeech, };
39
+ export { continuationSafetyProtocol, cancelFormTask, contextualResponseProtocol, validateFormResponse, hasFormResponse, formProposalPolicy, stageFormProposal, nextFormProposal, formProposalReadings, bindFormProposal, decideFormProposal, commitFormProposals, settleFormProposalValidation, formProposalProtocol, validateFormProposalPolicy, validateFormProposalDescriptor, formProposalDescriptor, clearFormMisses, formNoOwner, missFormInput, formMustComplete, formInputPrompt, formAcknowledges, consumeFormFeedback, formConversationOutcome, readMoneyAmount, isMoneyAmount, moneyIntentAllows, isPastPaymentStatement, isPartialMoneyAnswer, validateMoneyOptions, takeChoiceOffer, saysConfirmWord, confirmOutOfOrderLine, matchesChoice, ANSWER_WORDS, CONFIRM_WORDS, isAnswer, isAgentRequest, isExitRequest, isOtherRequest, isSpelledFragment, isAmountAnswer, amountInAnswer, pickChoice, withoutPromptEcho, readEmail, readPhone, readInternationalPhone, readIdentifier, last4, maskEmail, callerTextNumber, textDestination, callerIsInternational, normalizeAndFindCapture, anythingElse, goodbye, sayName, sorryNotCaught, dateToWords, amountToSpeech, validateCardNumber, validateExpiration, consumeActionReceipt, authorizeFormActionRetry, formSignature, selectFormAction, confirmFormRevision, prepareFormAction, actionConfirmationValid, beginFormAction, finishFormAction, actionGateVerified, sourceNeedsLoad, beginSourceRefresh, completeSourceRefresh, pickProviderQuestion, matchInputs, readFormAnswer, readInputProfile, formAcceptanceTrace, 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, answerResetsFormReplays, rejectFormReadings, invalidateFormInputs, acceptFormReadings, formInstanceApplies, formParentResume, closeFormInstance, checkOtp, clearOtp, maskDestination, validateOTP, OTP_EXPIRY_MS, validateDigits, validatePhone, validateEmail, urlToSpeech, textWithUrlToSpeech, joinSpokenDigits, spokenDigitsText, wholeSpokenNumber, };
39
40
  /** The functions flows call by name, ready to spread into a host's approved functions. */
40
41
  export const STANDARD_FUNCTIONS = {
41
42
  continuationSafetyProtocol, cancelFormTask, contextualResponseProtocol, validateFormResponse, hasFormResponse,
@@ -44,5 +45,5 @@ export const STANDARD_FUNCTIONS = {
44
45
  readMoneyAmount, isMoneyAmount, moneyIntentAllows, isPastPaymentStatement, isPartialMoneyAnswer, validateMoneyOptions, takeChoiceOffer, saysConfirmWord, confirmOutOfOrderLine, matchesChoice, isAnswer, isAgentRequest, isExitRequest, isOtherRequest, isSpelledFragment, isAmountAnswer, amountInAnswer, pickChoice, withoutPromptEcho,
45
46
  readEmail, readPhone, readInternationalPhone, readIdentifier, last4, maskEmail, callerTextNumber, textDestination, callerIsInternational,
46
47
  normalizeAndFindCapture, anythingElse, goodbye, sayName, sorryNotCaught, dateToWords, amountToSpeech, validateCardNumber, validateExpiration, consumeActionReceipt, authorizeFormActionRetry, formSignature, selectFormAction, confirmFormRevision, prepareFormAction, actionConfirmationValid, beginFormAction, finishFormAction, actionGateVerified, sourceNeedsLoad, beginSourceRefresh, completeSourceRefresh, pickProviderQuestion, matchInputs, readFormAnswer, readInputProfile, formAcceptanceTrace, 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, answerResetsFormReplays, rejectFormReadings, invalidateFormInputs, acceptFormReadings, formInstanceApplies, formParentResume, closeFormInstance,
47
- validateOTP, validateDigits, validatePhone, validateEmail, urlToSpeech, textWithUrlToSpeech,
48
+ validateOTP, validateDigits, validatePhone, validateEmail, urlToSpeech, textWithUrlToSpeech, joinSpokenDigits, wholeSpokenNumber,
48
49
  };
@@ -1 +1 @@
1
- {"version":3,"file":"match-inputs.d.ts","sourceRoot":"","sources":["../../src/helpers/match-inputs.js"],"names":[],"mappings":"AAiBA;;;;GAIG;AACH,sEAQC;AAwFD;;;GAGG;AACH,sFA8GC;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH;mHACmH;AAEnH,iEAEC;AAGD,gGAKC;AAED;;;;;;;;;;;;;;;;;;;;;;EAeC;AACD;;;;;;;;;;;;;;;;;;;;;;;EA+SC"}
1
+ {"version":3,"file":"match-inputs.d.ts","sourceRoot":"","sources":["../../src/helpers/match-inputs.js"],"names":[],"mappings":"AAkBA;;;;GAIG;AACH,sEAQC;AAwFD;;;GAGG;AACH,sFAuHC;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH;mHACmH;AAEnH,iEAEC;AAGD,gGAKC;AAED;;;;;;;;;;;;;;;;;;;;;;EAeC;AACD;;;;;;;;;;;;;;;;;;;;;;;EA+SC"}
@@ -14,6 +14,7 @@ import { readEmail, readPhone, readInternationalPhone, readIdentifier } from './
14
14
  import { readSpokenMoney } from './spoken-money.js';
15
15
  import { readMoneyAmount, isPartialMoneyAnswer } from './money.js';
16
16
  import { pickProviderQuestion } from './form-sources.js';
17
+ import { spokenDigitsText, wholeSpokenNumber } from './spoken-digits.js';
17
18
  /**
18
19
  * This turn's acceptance decisions for the host to log, appended per match: fields filled out of order, in a compound
19
20
  * answer or as an invited input, and every refused reading, by form, field and rule. Field names and rules only, never
@@ -137,7 +138,9 @@ export function readInputProfile(profile, answer, isCurrent = true) {
137
138
  if (kind === 'choice') {
138
139
  // An amount's own digits are the amount's: a number beside it ("51:72.45", sub account 51 and $72.45) is still a choice.
139
140
  const free = isAmountAnswer(raw) ? amountMasked(raw) : raw;
140
- const runs = (free.match(CHOICE_RUN) || []).map((r) => r.replace(/\D/g, '')), joined = free.replace(CHOICE_RUN, run => ' ' + run.replace(/\D/g, '') + ' ');
141
+ // A declared number of three or more digits said in pieces is that number ("6, 5, 4, 6"); "1, 2" stays two keys.
142
+ const whole = wholeSpokenNumber(free), pieces = whole !== free && whole.length >= 3 ? whole : '';
143
+ const runs = [...(free.match(CHOICE_RUN) || []).map((r) => r.replace(/\D/g, '')), ...(pieces ? [pieces] : [])], joined = pieces ? ` ${pieces} ` : free.replace(CHOICE_RUN, run => ' ' + run.replace(/\D/g, '') + ' ');
141
144
  const choices = (Array.isArray(profile.choices) ? profile.choices : []).filter(c => isCurrent || c.current_only !== true).map(c => ({ ...c,
142
145
  numericWords: (c.phrases ?? c.words ?? []).filter(word => /^\d{2,}$/.test(String(word).trim())),
143
146
  phrases: (c.phrases ?? c.words ?? []).concat(isCurrent ? c.keys || [] : []).filter(word => !(free !== raw && /^\d+$/.test(String(word).trim()) && !runs.includes(String(word).trim())) && (isCurrent || profile.numeric_values === true || !/^[\d*#]$/.test(String(word).trim()))) }));
@@ -173,12 +176,16 @@ export function readInputProfile(profile, answer, isCurrent = true) {
173
176
  if (isPartialMoneyAnswer(raw))
174
177
  return { value: raw, incompleteMoney: true, spans: [[0, raw.length]], evidence: ['partial-money'] };
175
178
  const read = money ? text => readMoneyAmount(text, money) : amountInAnswer;
176
- const n = (profile.marked === 'out_of_order' ? !isCurrent : profile.marked) ? (isAmountAnswer(raw) ? read(raw) : NaN) : read(raw);
179
+ // A number said in pieces is the same number; the rules decide as for that number.
180
+ const whole = wholeSpokenNumber(raw);
181
+ const spelled = whole !== raw;
182
+ const text = spelled ? whole : raw.replace(/^\s*\d(?:(?:\s+|,\s*|-|\.\s+)\d)+(?=\s*(?:dollars?|d[oó]lar(?:es)?|bucks?)\b)/i, (digits) => digits.replace(/\D/g, ''));
183
+ const n = (profile.marked === 'out_of_order' ? !isCurrent : profile.marked) ? (isAmountAnswer(text) ? read(text) : NaN) : read(text);
177
184
  const inRange = !isNaN(n) && (profile.min === undefined || n >= Number(profile.min)) && (profile.max === undefined || n <= Number(profile.max));
178
185
  if (inRange) {
179
186
  // the digits it was read from: the runs that start with its whole-dollar part ("82.50" said as "82 50" → 8250)
180
187
  const runs = digitRuns(raw).filter((d) => d.indexOf(String(Math.trunc(n))) === 0);
181
- return { value: String(n), spans: [...amountSpans(raw, n), ...(money?.cardinals ? readSpokenMoney(raw).filter(reading => reading.value === n).map(reading => [reading.start, reading.end]) : [])], evidence: runs.length ? runs.map((d) => `d:${d}`) : ['amount'] };
188
+ return { value: String(n), spans: [...(spelled ? [[raw.search(/\d/), raw.search(/\d[^\d]*$/) + 1]] : amountSpans(raw, n)), ...(money?.cardinals ? readSpokenMoney(raw).filter(reading => reading.value === n).map(reading => [reading.start, reading.end]) : [])], evidence: runs.length ? runs.map((d) => `d:${d}`) : ['amount'] };
182
189
  }
183
190
  else {
184
191
  return null;
@@ -231,19 +238,22 @@ export function readInputProfile(profile, answer, isCurrent = true) {
231
238
  }
232
239
  }
233
240
  else if (kind === 'pattern') {
241
+ // spoken_digits: the pattern reads a number said in pieces as one number ("5 0 9 Lazy Lane", "apt 2 0 4").
242
+ const said = profile.spoken_digits ? spokenDigitsText(raw, profile.spoken_digits) : { text: raw, at: (i) => i };
234
243
  let m = null;
235
244
  try {
236
- m = new RegExp(profile.regex, profile.flags || 'i').exec(raw);
245
+ m = new RegExp(profile.regex, profile.flags || 'i').exec(said.text);
237
246
  }
238
247
  catch {
239
248
  m = null; // an invalid pattern in a spec reads nothing
240
249
  }
241
250
  if (m) {
242
251
  const v = m[profile.group || 0] || m[0];
243
- return { value: v, spans: [[m.index + m[0].indexOf(v), m.index + m[0].indexOf(v) + v.length]], evidence: [`p:${String(v).toLowerCase()}`] };
252
+ const start = m.index + m[0].indexOf(v);
253
+ return { value: v, spans: [[said.at(start), said.at(start + v.length - 1) + 1]], evidence: [`p:${String(v).toLowerCase()}`] };
244
254
  }
245
255
  else {
246
- return isCurrent && profile.accept_other === true && raw.trim() ? { value: raw.trim(), fallback: true, spans: [[0, raw.length]] } : null;
256
+ return isCurrent && profile.accept_other === true && raw.trim() ? { value: said.text.trim(), fallback: true, spans: [[0, raw.length]] } : null;
247
257
  }
248
258
  }
249
259
  else if (kind === 'text') {
@@ -0,0 +1,12 @@
1
+ /** { text, at }: the text with its spoken numbers joined, and at(i), the index in the original of text index i. */
2
+ export function spokenDigitsText(input: any, mode?: boolean): {
3
+ text: string;
4
+ at: (i: any) => any;
5
+ };
6
+ export function joinSpokenDigits(input: any, mode?: boolean): string;
7
+ /**
8
+ * A whole answer that is one number said digit by digit reads as that number: "6 5 4 6", "6, 5, 4, 6.", "6-5-4-6", "6. 5. 4. 6.".
9
+ * Anything else comes back as it was. A period with no space after it stays a decimal point ("4.5").
10
+ */
11
+ export function wholeSpokenNumber(input: any): string;
12
+ //# sourceMappingURL=spoken-digits.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"spoken-digits.d.ts","sourceRoot":"","sources":["../../src/helpers/spoken-digits.js"],"names":[],"mappings":"AAoBA,mHAAmH;AACnH;;;EAyBC;AAED,qEAEC;AAED;;;GAGG;AACH,sDAGC"}
@@ -0,0 +1,67 @@
1
+ /**
2
+ * One number said in pieces, joined (owner 2026-11-03: every numeric reader takes spaces). Speech-to-text writes spoken
3
+ * digits apart ("5 0 9"), in groups ("50 9"), or with the pause as a comma, a period or a dash ("5, 0, 9.", "5-0-9").
4
+ * A dash joins only single digits: "12-34 Main St" is a hyphenated house number, not a pause.
5
+ *
6
+ * mode true: any run of groups of up to three digits. mode 'house_number': the first group has at most two digits,
7
+ * because "509 12 Ave" is a house number and a numbered street. Part of jsfe/helpers; zero imports.
8
+ */
9
+ const RUN = /(?<![\w.,-])\d{1,3}(?:(?:[ \t]+|,\s*|\.\s+|-)\d{1,3})+(?![\w-])/g;
10
+ const joins = (run, mode) => {
11
+ const groups = run.split(/\D+/);
12
+ if (run.includes('-') && !groups.every((group) => group.length === 1)) {
13
+ return false;
14
+ }
15
+ else if (mode === 'house_number') {
16
+ return groups[0].length <= 2;
17
+ }
18
+ else {
19
+ return true;
20
+ }
21
+ };
22
+ /** { text, at }: the text with its spoken numbers joined, and at(i), the index in the original of text index i. */
23
+ export function spokenDigitsText(input, mode = true) {
24
+ const raw = String(input ?? '');
25
+ const cuts = [...raw.matchAll(RUN)].filter((match) => joins(match[0], mode));
26
+ let text = '';
27
+ const origin = [];
28
+ let from = 0;
29
+ const keep = (start, end) => {
30
+ for (let i = start; i < end; i++) {
31
+ text += raw[i];
32
+ origin.push(i);
33
+ }
34
+ };
35
+ for (const cut of cuts) {
36
+ keep(from, cut.index);
37
+ for (let i = cut.index; i < cut.index + cut[0].length; i++) {
38
+ if (/\d/.test(raw[i])) {
39
+ text += raw[i];
40
+ origin.push(i);
41
+ }
42
+ else { /* a pause inside the number */ }
43
+ }
44
+ from = cut.index + cut[0].length;
45
+ }
46
+ keep(from, raw.length);
47
+ // A pause after the house number: "2 18, Nice Dr" (prod 11-02 CA120bcd), "633, North Normandy".
48
+ const pause = mode === 'house_number' ? /^\s*\d{1,6}([,.])\s/.exec(text) : null;
49
+ if (pause) {
50
+ const cut = pause[0].length - 2;
51
+ text = text.slice(0, cut) + text.slice(cut + 1);
52
+ origin.splice(cut, 1);
53
+ }
54
+ else { /* no pause there */ }
55
+ return { text, at: (i) => (i < origin.length ? origin[i] : raw.length) };
56
+ }
57
+ export function joinSpokenDigits(input, mode = true) {
58
+ return spokenDigitsText(input, mode).text;
59
+ }
60
+ /**
61
+ * A whole answer that is one number said digit by digit reads as that number: "6 5 4 6", "6, 5, 4, 6.", "6-5-4-6", "6. 5. 4. 6.".
62
+ * Anything else comes back as it was. A period with no space after it stays a decimal point ("4.5").
63
+ */
64
+ export function wholeSpokenNumber(input) {
65
+ const text = String(input ?? '');
66
+ return /^\s*\d(?:(?:\s+|,\s*|-|\.\s+)\d)+[\s.]*$/.test(text) ? text.replace(/\D/g, '') : text;
67
+ }
@@ -1 +1 @@
1
- export const JSFE_RUNTIME_FINGERPRINT = "sha256:4a74cb1db2b7f66f369ea39515417f0a7aea044e834e78cb2cad695f13af9359";
1
+ export const JSFE_RUNTIME_FINGERPRINT = "sha256:b99d629fc173a8b09100822cca9c1f36bdac8aed60ac2806fe4da0a0540e6bba";
@@ -193,7 +193,7 @@ Profiles:
193
193
  | `choice` | `choices:[{key,phrases,label?,label_es?,keys?,exact?,neutral?,negative?,current_only?}]`; optional `accept_other`, `numeric_values`, `narrative`, `whole_phrases`, `template_words`. |
194
194
  | `digits` | `min`, `max`: one unambiguous digit run of that length. |
195
195
  | `identifier` | `pattern`: permitted account identifier shape. |
196
- | `pattern` | `regex`, optional `flags`, `group`; optional `accept_other` for the current field only, `whole_phrases`, `template_words`. |
196
+ | `pattern` | `regex`, optional `flags`, `group`; optional `accept_other` for the current field only, `whole_phrases`, `template_words`; `spoken_digits` (`true`, or `'house_number'` when a numbered street can follow) reads a number said in pieces as one number ("5 0 9 Lazy Lane", "apt 2 0 4"), required on any pattern that reads digits. |
197
197
  | `phone`, `email` | Typed readers; email `literal:true` preserves an explicit visible address token in surrounding prose. |
198
198
  | `amount` | `min`, `max`, optional `marked:true` or `marked:"out_of_order"`; optional `money:{currency:"USD",cardinals:true,intent:"payment"}`. |
199
199
  | `text` | Current-field text fallback; `narrative:true` retains a substantive answer alongside independent structured fields. |
@@ -217,6 +217,7 @@ Declarations:
217
217
  | input `confirmation:"firm"` / `"soft"` | A yes/no decision. Firm (consent to a charge, a cancellation, storing a card, filing): YES only by `CONFIRM_WORDS` or the option's own exact phrases. Soft (an offer): the wider YES vocabulary. NO by the NO vocabulary or the option's own phrases. |
218
218
  | input `invites:[names]`, `form.confirm.invites` | The other inputs this question (or the read-back) also asks for ("NEW CARD, LINK, or a different amount" invites `amount`), in its form or an enclosing one; read as in order while it is asked. |
219
219
  | profile `out_of_order:{…}` | A narrower signature for out-of-order reading: the payment card takes a bare last four when asked, but out of order only a card word or "ending in 4242". |
220
+ | `authenticate-user` `other_whole_answer:true` | At the code question "another account" switches only as the whole answer; inside a sentence the reply ranker decides, offered `another_account` and `no_other_account` ("I understand. We still need to secure access to your account.", then the question). Default off. |
220
221
  | input `confirm_out_of_order:{word,word_es,message,message_es}` | Out of order a number fills this input only beside its `word`, in either language. Alone, the caller hears `message` (`{value}` is the number, `{word}` the word) and the current question again, which counts as an attempt. `authenticate-user`'s `code_confirm` is the same declaration for a `code_validator`, whose flow reads it with `saysConfirmWord` and `confirmOutOfOrderLine`: an account number at the verification-code question, where it may be a code said with an extra digit. |
221
222
  | input `choices_from` | The choice's values come from prepared data (the caller's saved cards), so its signature is those values. |
222
223
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jsfe",
3
- "version": "1.32.0",
3
+ "version": "1.34.0",
4
4
  "description": "TypeScript workflow engine for conversational/automation apps: multi-flow orchestration, interrupt/resume, stack-based control, robust errors, and flexible tool/REST integration.",
5
5
  "license": "MIT",
6
6
  "author": "Ron Pinkas",