jsfe 1.32.0 → 1.33.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,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:615ae9b91c5c26356b7d94f1274e486dff28902a1a3f0c8311d258dc62eade9f";
@@ -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. |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jsfe",
3
- "version": "1.32.0",
3
+ "version": "1.33.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",