jsfe 1.9.3 → 1.10.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.
@@ -165,6 +165,7 @@ export function validateFlowContracts(flows: any, tools?: any[], { externalFlows
165
165
  label: string;
166
166
  label_es: string;
167
167
  accumulate: boolean;
168
+ accumulate_first: string;
168
169
  profile: {
169
170
  kind: string;
170
171
  min: number;
@@ -1 +1 @@
1
- {"version":3,"file":"contracts.d.ts","sourceRoot":"","sources":["../../src/forms/contracts.js"],"names":[],"mappings":"AAgBA;;;IAGC;AACD;;;;;EAGC;AACD,qGAAqG;AACrG;;QAwCC;AAqBD;;;QAEC;AAgBD;;;;;;;;;QASC;AACD;;;;QAGC;AACD;;;;;;;;QA8BC;AAED,mHAAmH;AACnH,kDAgBC;AAED,+GAA+G;AAC/G;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA2EC"}
1
+ {"version":3,"file":"contracts.d.ts","sourceRoot":"","sources":["../../src/forms/contracts.js"],"names":[],"mappings":"AAgBA;;;IAGC;AACD;;;;;EAGC;AACD,qGAAqG;AACrG;;QAwCC;AAqBD;;;QAEC;AAgBD;;;;;;;;;QASC;AACD;;;;QAGC;AACD;;;;;;;;QA8BC;AAED,mHAAmH;AACnH,kDAgBC;AAED,+GAA+G;AAC/G;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA2EC"}
@@ -1,10 +1,3 @@
1
- /**
2
- * The form shell: declare inputs, run the loop, hand the result on. `after` = the form's own steps once complete.
3
- * `confirm` ({ question, question_es } — SAY-style templates that may read cargo.forms[<id>].values; build them in
4
- * `before_confirm` SET steps when they need computing): the exact confirmation, asked every time after the loop and
5
- * never an input. YES runs `after`; NO runs `declined`; a correction given there goes through the form's own matching
6
- * and the form runs again (so whatever the correction made missing is asked).
7
- */
8
1
  export function formFlow({ id, name, version, description, inputs, after, extraVars, parameters, confirm, before_confirm, declined, stop, close_on_stop, resume_on_parent_change, before, done, primary, scope, validator, confirmation_condition, prompt, prompt_es, incomplete_outcome, on_exhausted }: {
9
2
  id: any;
10
3
  name: any;
@@ -265,6 +258,7 @@ export namespace FIELD_PRESETS {
265
258
  let label_es_9: string;
266
259
  export { label_es_9 as label_es };
267
260
  export let accumulate: boolean;
261
+ export let accumulate_first: string;
268
262
  export namespace profile_9 {
269
263
  let kind_9: string;
270
264
  export { kind_9 as kind };
@@ -1 +1 @@
1
- {"version":3,"file":"flows.d.ts","sourceRoot":"","sources":["../../src/forms/flows.js"],"names":[],"mappings":"AA4FA;;;;;;GAMG;AACH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA0DC;AACD;;;GAGG;AACH;;;;;;EAGC;AAuYD,wHAAwH;AACxH,wEAaC;AACD,4FAA4F;AAC5F;;;EAYC;AACD,sGAAsG;AACtG;;;EAOC;AAviBM,6CAAuG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8tB9G;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAOG"}
1
+ {"version":3,"file":"flows.d.ts","sourceRoot":"","sources":["../../src/forms/flows.js"],"names":[],"mappings":"AAqHA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA2DC;AACD;;;GAGG;AACH;;;;;;EAGC;AAyYD,wHAAwH;AACxH,wEAaC;AACD,4FAA4F;AAC5F;;;EAYC;AACD,sGAAsG;AACtG;;;EAOC;AA5jBM,6CAAuG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmvB9G;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAOG"}
@@ -65,7 +65,7 @@ export const FIELD_PRESETS = {
65
65
  city: { label: 'city', label_es: 'ciudad', profile: { kind: 'text' }, question: 'What city?', question_es: '¿En qué ciudad?' },
66
66
  state: { label: 'state', label_es: 'estado', profile: { kind: 'pattern', regex: '^\\s*(?:Alabama|Alaska|Arizona|Arkansas|California|Colorado|Connecticut|Delaware|Florida|Georgia|Hawaii|Idaho|Illinois|Indiana|Iowa|Kansas|Kentucky|Louisiana|Maine|Maryland|Massachusetts|Michigan|Minnesota|Mississippi|Missouri|Montana|Nebraska|Nevada|New Hampshire|New Jersey|New Mexico|New York|North Carolina|North Dakota|Ohio|Oklahoma|Oregon|Pennsylvania|Rhode Island|South Carolina|South Dakota|Tennessee|Texas|Utah|Vermont|Virginia|Washington|West Virginia|Wisconsin|Wyoming|District of Columbia|Puerto Rico|AL|AK|AZ|AR|CA|CO|CT|DE|FL|GA|HI|ID|IL|IN|IA|KS|KY|LA|ME|MD|MA|MI|MN|MS|MO|MT|NE|NV|NH|NJ|NM|NY|NC|ND|OH|OK|OR|PA|RI|SC|SD|TN|TX|UT|VT|VA|WA|WV|WI|WY|DC|PR)\\s*$' },
67
67
  question: 'What state?', question_es: '¿En qué estado?' },
68
- card_number: { label: 'card number', label_es: 'número de tarjeta', accumulate: true, profile: { kind: 'digits', min: 13, max: 19 }, digits: { min: 13, max: 19, complete: [{ first: '4', lengths: [16, 19] }, { first: '25', lengths: [16] }, { first: '3', lengths: [15] }, { first: '6', lengths: [16, 19] }] },
68
+ card_number: { label: 'card number', label_es: 'número de tarjeta', accumulate: true, accumulate_first: '^[2-6]', profile: { kind: 'digits', min: 13, max: 19 }, digits: { min: 13, max: 19, complete: [{ first: '4', lengths: [16, 19] }, { first: '25', lengths: [16] }, { first: '3', lengths: [15] }, { first: '6', lengths: [16, 19] }] },
69
69
  pii: true, validator: 'validate-card-number',
70
70
  question: 'Please say or enter your card number.', question_es: 'Por favor diga o ingrese el número de su tarjeta.',
71
71
  retry: 'Please say or enter the full card number.', retry_es: 'Por favor diga o ingrese el número completo de su tarjeta.' },
@@ -91,8 +91,29 @@ const FIELD_COLLECTORS = { 'collect-phone': 'phone', 'collect-email': 'email', '
91
91
  * never an input. YES runs `after`; NO runs `declined`; a correction given there goes through the form's own matching
92
92
  * and the form runs again (so whatever the correction made missing is asked).
93
93
  */
94
+ /**
95
+ * Context-aware acceptance (docs/copilot-reference.md §5) is standard for every form; its declarations are checked here,
96
+ * at definition time: a confirmation is a firm or soft choice keyed yes and no; invites list input names (of this form
97
+ * or an enclosing one, resolved at run time by scope).
98
+ */
99
+ function validateAcceptanceDeclarations(id, inputs) {
100
+ for (const input of (inputs || []).flatMap((i) => i.any_of || [i])) {
101
+ if (input.confirmation !== undefined) {
102
+ const keys = (input.profile?.choices || []).map((choice) => choice.key);
103
+ if (!['firm', 'soft'].includes(input.confirmation) || input.profile?.kind !== 'choice' || !keys.includes('yes') || !keys.includes('no'))
104
+ throw new Error(`FORM ${id}: ${input.name} confirmation must be "firm" or "soft" on a choice keyed yes and no`);
105
+ }
106
+ else {
107
+ // not a confirmation
108
+ }
109
+ if (input.invites !== undefined && (!Array.isArray(input.invites) || !input.invites.every((name) => typeof name === 'string' && name)))
110
+ throw new Error(`FORM ${id}: ${input.name} invites must list input names`);
111
+ else { /* no invites, or a list of names */ }
112
+ }
113
+ }
94
114
  export function formFlow({ id, name, version, description, inputs, after = [], extraVars = {}, parameters = [], confirm = null, before_confirm = [], declined = [], stop = '', close_on_stop = false, resume_on_parent_change = false, before = [], done = true, primary = false, scope = 'isolated', validator = '', confirmation_condition = 'true', prompt = undefined, prompt_es = undefined, incomplete_outcome = null, on_exhausted = null }) {
95
115
  validateFormExhaustion(on_exhausted);
116
+ validateAcceptanceDeclarations(id, inputs);
96
117
  const incompleteFrom = incomplete_outcome && Object.hasOwn(incomplete_outcome, 'from');
97
118
  if (incompleteFrom) {
98
119
  if (Object.keys(incomplete_outcome).length !== 1 || typeof incomplete_outcome.from !== 'string' || !/^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*$/.test(incomplete_outcome.from) || !parameters.some(p => p.name === incomplete_outcome.from.split('.')[0] && p.type === 'object'))
@@ -105,7 +126,7 @@ export function formFlow({ id, name, version, description, inputs, after = [], e
105
126
  // FLOW parameters are paths only: the question is built here, as a SAY-style template, then passed by name
106
127
  SET(`${id}_confirm_q`, 'form_confirm_q', textValue(confirm.question)),
107
128
  SET(`${id}_confirm_q_es`, 'form_confirm_q_es', textValue(confirm.question_es || confirm.question)),
108
- FLOW(`${id}_confirm`, 'confirm-form', 'call', { form: '{{form_instance}}', question: '{{form_confirm_q}}', question_es: '{{form_confirm_q_es}}', phrases: '{{form_confirmation.phrases}}', words: '{{form_confirmation.words}}', clarifications: '{{form_confirmation.clarifications}}', no_keys: '{{form_confirmation.no_keys}}', interrupts: '{{form_confirmation.interrupts}}', named_fields: '{{form_confirmation.named_fields}}', exit_context: '{{exit_context}}', exit_context_es: '{{exit_context_es}}' }),
129
+ FLOW(`${id}_confirm`, 'confirm-form', 'call', { form: '{{form_instance}}', question: '{{form_confirm_q}}', question_es: '{{form_confirm_q_es}}', phrases: '{{form_confirmation.phrases}}', words: '{{form_confirmation.words}}', clarifications: '{{form_confirmation.clarifications}}', no_keys: '{{form_confirmation.no_keys}}', interrupts: '{{form_confirmation.interrupts}}', invites: '{{form_confirmation.invites}}', named_fields: '{{form_confirmation.named_fields}}', exit_context: '{{exit_context}}', exit_context_es: '{{exit_context_es}}' }),
109
130
  { id: `${id}_confirmed`, type: 'CASE', branches: {
110
131
  [`condition: (cargo.formConfirm || {})[form_instance] === 'changed'`]: FLOW(`${id}_again`, id, 'replace', { parent_form: '{{parent_form}}', result_input: '{{result_input}}', form_instance: '{{form_instance}}' }),
111
132
  // NO: the declined flow takes the form's place, so the action below never runs
@@ -253,10 +274,11 @@ const collectInput = {
253
274
  WHEN('ci_routed_end', 'ci_routed', END('ci_routed_done')),
254
275
  // `accumulate` (opt-in, digit inputs): a number said in groups across turns ("4347", "6971 2210"…) is kept and joined;
255
276
  // short of the input's minimum the next question is "Go on." Anything that is not digits drops the partial.
256
- SET('ci_partial_join', 'ci_partial', "(function () { var state = cargo.forms[form]; if (!ci_spec || ci_spec.accumulate !== true || !ci_spec.digits) return ''; var said = String(cargo.choiceText || '').trim(); var data = state.data = state.data || {}; var key = '__partial_' + current; if (!/^[0-9][0-9\\s.,-]*$/.test(said)) { delete data[key]; return ''; } var joined = String(data[key] || '') + said.replace(/[^0-9]/g, ''); if (joined.length < Number(ci_spec.digits.min || 0)) { data[key] = joined; var fb = state.feedback || {}; fb[current] = 'partial'; state.feedback = fb; return 'hold'; } delete data[key]; cargo.choiceText = joined; cargo.choice = joined; return 'joined'; })()"),
277
+ SET('ci_partial_join', 'ci_partial', "(function () { var state = cargo.forms[form]; if (!ci_spec || ci_spec.accumulate !== true || !ci_spec.digits) return ''; var said = String(cargo.choiceText || '').trim(); var data = state.data = state.data || {}; var key = '__partial_' + current; if (!/^[0-9][0-9\\s.,-]*$/.test(said)) { delete data[key]; return ''; } var joined = String(data[key] || '') + said.replace(/[^0-9]/g, ''); var starts = ci_spec.accumulate_first ? new RegExp(ci_spec.accumulate_first) : null; if (!data[key] && starts && !starts.test(joined)) return ''; if (joined.length < Number(ci_spec.digits.min || 0)) { data[key] = joined; var fb = state.feedback || {}; fb[current] = 'partial'; state.feedback = fb; return 'hold'; } delete data[key]; cargo.choiceText = joined; cargo.choice = joined; return 'joined'; })()"),
257
278
  WHEN('ci_partial_hold', "ci_partial === 'hold'", END('ci_partial_wait')),
258
279
  SET('ci_build_chain', 'ci_chain', CHAIN_JS),
259
280
  SET('ci_match', 'ci_m', "readFormAnswer(cargo.choiceText, ci_chain, Array.isArray(ci_spec.any_of) ? { form: form, names: ci_names } : { form: form, name: current }, cargo.choice, provides && Array.isArray(questions) ? questions : [])"),
281
+ SIDE('ci_acceptance_trace', "(cargo.formAcceptance = formAcceptanceTrace(cargo.formAcceptance, ci_m), true)"),
260
282
  // The labels and validators ride on each reading, so the presets stay in this one flow.
261
283
  SET('ci_enrich', 'ci_m', 'enrichFormMatches(ci_m, ci_chain, form, ci_names, current)'),
262
284
  SET('ci_question_key', 'ci_qkey', "ci_m.questions.length ? ci_m.questions[0] : !ci_m.hits.length && !ci_m.ambiguous.length && Array.isArray(questions) && questions.length && provides ? pickChoice(cargo.choiceText, questions.map(function(question){return Object.assign({},question,{exact:true});}), false) : ''"),
@@ -425,8 +447,8 @@ const disambiguate = {
425
447
  const confirmForm = {
426
448
  id: 'confirm-form', name: 'ConfirmForm', version: '1.0.0', scope: 'isolated',
427
449
  description: "Worker: ask a form's confirmation (exact YES / NO; keys 1 and 2). A correction instead ('no, make it $40') is read against the form chain and stored, and the result is 'changed' so the form runs again; an unclear short answer is asked again, a long unrelated one is DISPATCHed. Result: cargo.formConfirm[form] = 'yes' | 'no' | 'changed'. Params: form, question(_es), exit_context(_es).",
428
- parameters: ['form', 'question', 'question_es', 'exit_context', 'exit_context_es', 'phrases', 'words', 'interrupts', 'no_keys', 'clarifications'].map((n) => ({ name: n, type: ['phrases', 'words', 'interrupts', 'no_keys', 'clarifications'].includes(n) ? 'array' : 'string', description: n })).concat([{ name: 'named_fields', type: 'boolean', description: "true: an answer that names one of the form's inputs (its names) without a value re-asks only that input" }]),
429
- variables: vars({ named_fields: false, cf_named: null, cf_presets: FIELD_PRESETS, cf_chain: [], cf_m: null, cf_current: null, cf_result: '', cf_prompt: '', cf_prompt_es: '', cf_words: 0, cf_tries: 0, phrases: null, words: [], clarifications: [], no_keys: ['2'], interrupts: [], cf_choices: [], cf_event: '', cf_amount: false, cf_questions: [], cf_provides: null, cf_skip_gate: '' }),
450
+ parameters: ['form', 'question', 'question_es', 'exit_context', 'exit_context_es', 'phrases', 'words', 'interrupts', 'no_keys', 'clarifications', 'invites'].map((n) => ({ name: n, type: ['phrases', 'words', 'interrupts', 'no_keys', 'clarifications', 'invites'].includes(n) ? 'array' : 'string', description: n })).concat([{ name: 'named_fields', type: 'boolean', description: "true: an answer that names one of the form's inputs (its names) without a value re-asks only that input" }]),
451
+ variables: vars({ named_fields: false, cf_named: null, cf_presets: FIELD_PRESETS, cf_chain: [], cf_m: null, cf_current: null, cf_result: '', cf_prompt: '', cf_prompt_es: '', cf_words: 0, cf_tries: 0, phrases: null, words: [], clarifications: [], no_keys: ['2'], interrupts: [], invites: [], cf_choices: [], cf_event: '', cf_amount: false, cf_questions: [], cf_provides: null, cf_skip_gate: '' }),
430
452
  steps: [
431
453
  SIDE('cf_reset', "(cargo.formConfirm = cargo.formConfirm || {}, cargo.formConfirm[form] = '', true)"),
432
454
  SET('cf_choices', 'cf_choices', `[{key:'yes',phrases:['1'].concat(Array.isArray(phrases)?phrases:Array.isArray(words)?words:[]),exact:true},{key:'no',phrases:(Array.isArray(no_keys)?no_keys:['2']).concat(${JSON.stringify(ANSWER_WORDS.no)}),exact:true}].concat((Array.isArray(interrupts)?interrupts:[]).map(function(event){return {key:'event:'+event.key,phrases:event.phrases ?? event.words,exact:event.exact!==false};}))`),
@@ -443,8 +465,9 @@ const confirmForm = {
443
465
  SET('cf_question_catalog', 'cf_questions', 'cargo.forms[form].questions || []'),
444
466
  SET('cf_provider_catalog', 'cf_provides', 'cargo.forms[form].providers'),
445
467
  SET('cf_gate_policy', 'cf_skip_gate', "!cf_provides || !cf_provides.gate || [].concat(cf_provides.gate.verified || []).every(function(p){return !!formPath({cargo:cargo},p);}) ? 'true' : ''"),
446
- SET('cf_current', 'cf_current', "cargo.forms[form].proposalPrompt ? {form:cargo.forms[form].parent,name:cargo.forms[form].proposalPrompt.input} : {form:form,name:''}"),
468
+ SET('cf_current', 'cf_current', "cargo.forms[form].proposalPrompt ? {form:cargo.forms[form].parent,name:cargo.forms[form].proposalPrompt.input} : {form:form,name:'',invites:Array.isArray(invites)?invites:[]}"),
447
469
  SET('cf_match', 'cf_m', "cargo.choice === 'yes' || cargo.choice === 'no' && pickChoice(cargo.choiceText, [{key:'no',phrases:Array.isArray(no_keys)?no_keys:['2'],exact:true}], false) === 'no' ? { hits: [], ambiguous: [], questions:[] } : readFormAnswer(cargo.choiceText, cf_chain, cf_current, '',cf_questions)"),
470
+ SIDE('cf_acceptance_trace', "(cargo.formAcceptance = formAcceptanceTrace(cargo.formAcceptance, cf_m), true)"),
448
471
  SET('cf_enrich', 'cf_m', "(function(m){m.hits=m.hits.map(function(h){return Object.assign({},h,{silent:true});});return m;})(enrichFormMatches(cf_m, cf_chain, form))"),
449
472
  SET('cf_count_words', 'cf_words', "String(cargo.choiceText || '').split(/\\s+/).filter(function (w) { return /[a-záéíóúüñ]/i.test(w); }).length"),
450
473
  // opt-in (confirm.named_fields): 'the city is wrong' / 'la ciudad no' names exactly one input and gives no value; that
@@ -1 +1 @@
1
- {"version":3,"file":"identity-flows.d.ts","sourceRoot":"","sources":["../../src/forms/identity-flows.js"],"names":[],"mappings":"AA2BA,+GAA+G;AAC/G,oFAOC;AAQD,wFAAwF;AACxF,iDAGC;AACD,6CAMC;AACD,qHAQC;AAMD,4EAKC;AAgFD,4DAKC;AA+ED,8EAGC;AA4BD,4FAA4F;AAC5F;;;;EAOC;AAQD,0BAAmI"}
1
+ {"version":3,"file":"identity-flows.d.ts","sourceRoot":"","sources":["../../src/forms/identity-flows.js"],"names":[],"mappings":"AA2BA,+GAA+G;AAC/G,oFAOC;AAQD,wFAAwF;AACxF,iDAGC;AACD,6CAMC;AACD,qHAQC;AAMD,4EAKC;AAgFD,4DAKC;AAiFD,8EAGC;AA4BD,4FAA4F;AAC5F;;;;EAOC;AAQD,0BAAmI"}
@@ -184,7 +184,9 @@ const code = worker('get-and-validate-otp-code', 'GetAndValidateOtpCode', 'Colle
184
184
  ] });
185
185
  function otpMenu(id, error, error_es, allow_code_entry = false) { const en = id + '_message', es = id + '_message_es'; otpMenuMessages.push([en, error], [es, error_es]); return call(id, 'generic-retry-with-options', { error_message: '{{' + en + '}}', error_message_es: '{{' + es + '}}', retry_flow: 'authenticate-user', retry_call_type: 'replace', cancel_flow: '{{cargo.authCancelFlow}}', allow_code_entry, capture_patterns: '{{cargo.authCapture}}', parent_form: '{{cargo.authOptions.parent_form}}', question: 'Would you like to try again to get access to your account?', question_es: '¿Le gustaría intentar nuevamente obtener acceso a su cuenta?' }, 'replace'); }
186
186
  const OTP_WORDS = {
187
- other: ['otra cuenta', 'otro número', 'otro numero', 'cuenta diferente', 'another account', 'different account', 'other account', 'another number', 'different number', 'not my account', 'no es mi cuenta'],
187
+ other: ['otra cuenta', 'otro número', 'otro numero', 'cuenta diferente', 'another account', 'different account', 'other account', 'another number', 'different number', 'not my account', 'no es mi cuenta', 'número de cuenta', 'numero de cuenta', 'account number'],
188
+ // the offered OTRA CUENTA / ANOTHER ACCOUNT said by its first word, as the whole answer (prod 2026-10-25 00:19: "Otra")
189
+ other_alone: ['otra', 'otro', 'another', 'other'],
188
190
  resend: ['resend', 'send again', 'send it again', 'not received', 'never received', 'did not receive', 'didn t receive', 'didn t get', 'haven t received', 'nothing came', 'no code', 'no text', 'no llegó', 'no llego', 'no llega', 'no ha llegado', 'no me llegó', 'no me llego', 'no le llegó', 'no le llego', 'no me ha llegado', 'no recibí', 'no recibi', 'no lo recibí', 'no lo recibi', 'no tengo el código', 'no tengo el codigo', 'no lo tengo', 'no alcancé', 'no alcance', 'mándame el código', 'mandame el codigo', 'envíame el código', 'enviame el codigo', 'reenviar', 'reenvíe', 'reenvie'],
189
191
  later: ['driving', 'i am driving', 'i m busy', 'im busy', 'not now', 'later', 'call me later', 'call back', 'cant right now', 'can t right now', 'can t now', 'cannot right now', 'manejando', 'conduciendo', 'estoy manejando', 'voy manejando', 'ahora no', 'ahora no puedo', 'no puedo ahora', 'más tarde', 'mas tarde', 'estoy ocupado', 'estoy ocupada'],
190
192
  noPhone: ['don t have that phone', 'don t have that number', 'don t have that phone anymore', 'don t have the phone', 'not my number anymore', 'not my phone anymore', 'that s an old number', 'old number', 'changed my number', 'changed my phone', 'lost my phone', 'no tengo ese teléfono', 'no tengo ese telefono', 'no tengo ese número', 'no tengo ese numero', 'ya no tengo ese', 'ya no tengo el teléfono', 'ya no tengo el telefono', 'ya no es mi número', 'ya no es mi numero', 'número viejo', 'numero viejo', 'cambié de número', 'cambie de numero', 'cambié mi número', 'perdí mi teléfono', 'perdi mi telefono', 'don t have a phone', 'don t have phone', 'don t have my phone', 'don t have a cell', 'don t have the cell', 'don t have my cell', 'don t have a cell phone', 'don t have access to that phone', 'don t have access to the phone', 'no tengo teléfono', 'no tengo telefono', 'no tengo celular', 'no tengo el celular', 'no tengo mi teléfono', 'no tengo mi telefono', 'no tengo mi celular', 'no tengo un teléfono', 'no tengo un telefono', 'no tengo acceso a ese teléfono', 'no tengo acceso a ese telefono']
@@ -201,7 +203,7 @@ const validateCode = worker('validate-otp-code', 'ValidateOtpCode', 'Validate a
201
203
  set('otp_reset_no_code', 'no_code_given', 'false'), set('otp_reset_invite', 'whatsapp_invite_sent', 'false'), set('otp_reset_callback', 'answer_key', "''"),
202
204
  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}}' })),
203
205
  set('otp_other_given', 'other_account_given', "answer_key === 'another_account' ? (cargo.authOtherAccount=String(answer_value || ''),true) : false"),
204
- set('otp_other_requested', 'wants_other_account', 'other_account_given || !!(cargo.callerPreselected && normalized_otp_code.length !== 6 && matchesChoice(user_choice,otp_phrases.other))'),
206
+ 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())))'),
205
207
  when('otp_tenant_answer', 'cargo.authOptions && cargo.authOptions.answer_handler && normalized_otp_code.length !== 6 && !wants_other_account', call('otp_tenant_answer_call', '{{cargo.authOptions.answer_handler}}', { answer: '{{user_choice}}' })),
206
208
  cases('otp_response', {
207
209
  'condition: normalized_otp_code.length === 6': { id: 'otp_check_tool', type: 'CALL-TOOL', tool: 'validate-otp', variable: 'otp_validation_result', args: { otp: '{{normalized_otp_code}}', container: '{{cargo}}' }, onFail: call('otp_check_failed', 'get-and-validate-otp-code', undefined, 'replace') },
@@ -71,6 +71,7 @@ export namespace STANDARD_FUNCTIONS {
71
71
  export { matchInputs };
72
72
  export { readFormAnswer };
73
73
  export { readInputProfile };
74
+ export { formAcceptanceTrace };
74
75
  export { resolveFormInputs };
75
76
  export { formInputApplies };
76
77
  export { formInputFilled };
@@ -187,6 +188,7 @@ import { pickProviderQuestion } from './form-sources.js';
187
188
  import { matchInputs } from './match-inputs.js';
188
189
  import { readFormAnswer } from './match-inputs.js';
189
190
  import { readInputProfile } from './match-inputs.js';
191
+ import { formAcceptanceTrace } from './match-inputs.js';
190
192
  import { resolveFormInputs } from './form-state.js';
191
193
  import { formInputApplies } from './form-state.js';
192
194
  import { formInputFilled } from './form-state.js';
@@ -232,5 +234,5 @@ import { validatePhone } from './validators.js';
232
234
  import { validateEmail } from './validators.js';
233
235
  import { urlToSpeech } from './url-speech.js';
234
236
  import { textWithUrlToSpeech } from './url-speech.js';
235
- export { continuationSafetyProtocol, cancelFormTask, contextualResponseProtocol, validateFormResponse, hasFormResponse, formProposalPolicy, stageFormProposal, nextFormProposal, formProposalReadings, bindFormProposal, decideFormProposal, commitFormProposals, settleFormProposalValidation, formProposalProtocol, validateFormProposalPolicy, validateFormProposalDescriptor, formProposalDescriptor, clearFormMisses, missFormInput, formInputPrompt, consumeFormFeedback, formConversationOutcome, readMoneyAmount, isMoneyAmount, moneyIntentAllows, isPastPaymentStatement, isPartialMoneyAnswer, validateMoneyOptions, takeChoiceOffer, matchesChoice, ANSWER_WORDS, CONFIRM_WORDS, isAnswer, isAgentRequest, isOtherRequest, isSpelledFragment, isAmountAnswer, amountInAnswer, pickChoice, readEmail, readPhone, readInternationalPhone, readIdentifier, last4, maskEmail, callerTextNumber, textDestination, callerIsInternational, normalizeAndFindCapture, anythingElse, goodbye, sayName, dateToWords, amountToSpeech, validateCardNumber, validateExpiration, consumeActionReceipt, authorizeFormActionRetry, formSignature, selectFormAction, confirmFormRevision, prepareFormAction, actionConfirmationValid, beginFormAction, finishFormAction, actionGateVerified, sourceNeedsLoad, beginSourceRefresh, completeSourceRefresh, pickProviderQuestion, matchInputs, readFormAnswer, readInputProfile, resolveFormInputs, formInputApplies, formInputFilled, nextFormInput, formInputChain, openFormInstance, validateFormIncompleteOutcome, formIncompleteOutcome, validateFormExhaustion, exhaustFormInput, formInitialReadings, orderFormReadings, proposedFormValues, applyFormValidation, formReadingDependencies, exclusiveFormReadings, enrichFormMatches, expandFormActivations, settleFormActivations, rememberFormArguments, resetFormInputs, hasFormValue, formPath, formValuesEqual, formReadingReady, formReadingRelevant, formPreparation, queueFormReadings, pendingFormReadings, formWorkLimit, rejectFormReadings, invalidateFormInputs, acceptFormReadings, formInstanceApplies, formParentResume, closeFormInstance, checkOtp, maskDestination, validateOTP, OTP_EXPIRY_MS, validateDigits, validatePhone, validateEmail, urlToSpeech, textWithUrlToSpeech };
237
+ export { continuationSafetyProtocol, cancelFormTask, contextualResponseProtocol, validateFormResponse, hasFormResponse, formProposalPolicy, stageFormProposal, nextFormProposal, formProposalReadings, bindFormProposal, decideFormProposal, commitFormProposals, settleFormProposalValidation, formProposalProtocol, validateFormProposalPolicy, validateFormProposalDescriptor, formProposalDescriptor, clearFormMisses, missFormInput, formInputPrompt, consumeFormFeedback, formConversationOutcome, readMoneyAmount, isMoneyAmount, moneyIntentAllows, isPastPaymentStatement, isPartialMoneyAnswer, validateMoneyOptions, takeChoiceOffer, matchesChoice, ANSWER_WORDS, CONFIRM_WORDS, isAnswer, isAgentRequest, isOtherRequest, isSpelledFragment, isAmountAnswer, amountInAnswer, pickChoice, readEmail, readPhone, readInternationalPhone, readIdentifier, last4, maskEmail, callerTextNumber, textDestination, callerIsInternational, normalizeAndFindCapture, anythingElse, goodbye, sayName, dateToWords, amountToSpeech, validateCardNumber, validateExpiration, consumeActionReceipt, authorizeFormActionRetry, formSignature, selectFormAction, confirmFormRevision, prepareFormAction, actionConfirmationValid, beginFormAction, finishFormAction, actionGateVerified, sourceNeedsLoad, beginSourceRefresh, completeSourceRefresh, pickProviderQuestion, matchInputs, readFormAnswer, readInputProfile, 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, rejectFormReadings, invalidateFormInputs, acceptFormReadings, formInstanceApplies, formParentResume, closeFormInstance, checkOtp, maskDestination, validateOTP, OTP_EXPIRY_MS, validateDigits, validatePhone, validateEmail, urlToSpeech, textWithUrlToSpeech };
236
238
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/helpers/index.js"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;2CACyC,iCAAiC;+BAI6C,wBAAwB;2CAHjE,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;gCACrB,wBAAwB;8BAAxB,wBAAwB;gCAAxB,wBAAwB;oCAAxB,wBAAwB;wCAAxB,wBAAwB;gCAmBhB,YAAY;8BAAZ,YAAY;kCAAZ,YAAY;uCAAZ,YAAY;qCAAZ,YAAY;qCAAZ,YAAY;gCAlB3G,mBAAmB;8BAiBrB,qBAAqB;6BAEkG,aAAa;8BAAb,aAAa;yBAAb,aAAa;+BAAb,aAAa;+BAAb,aAAa;kCAAb,aAAa;+BAAb,aAAa;+BAAb,aAAa;2BAAb,aAAa;0BACT,cAAc;0BAAd,cAAc;uCAAd,cAAc;+BAAd,cAAc;sBAAd,cAAc;0BAAd,cAAc;iCAAd,cAAc;gCAAd,cAAc;sCAAd,cAAc;wCAC/H,cAAc;6BACP,cAAc;wBAAd,cAAc;wBAAd,cAAc;4BACjC,iBAAiB;+BACd,uBAAuB;mCACC,kBAAkB;mCAAlB,kBAAkB;qCAxB+I,mBAAmB;yCAAnB,mBAAmB;8BAAnB,mBAAmB;iCAAnB,mBAAmB;oCAAnB,mBAAmB;kCAAnB,mBAAmB;wCAAnB,mBAAmB;gCAAnB,mBAAmB;iCAAnB,mBAAmB;mCAAnB,mBAAmB;gCAe1I,mBAAmB;mCAAnB,mBAAmB;sCAAnB,mBAAmB;qCAAnB,mBAAmB;4BAUtD,mBAAmB;+BAAnB,mBAAmB;iCAAnB,mBAAmB;kCAC+nB,iBAAiB;iCAAjB,iBAAiB;gCAAjB,iBAAiB;8BAAjB,iBAAiB;+BAAjB,iBAAiB;iCAAjB,iBAAiB;8CAAjB,iBAAiB;sCAAjB,iBAAiB;uCAAjB,iBAAiB;iCAAjB,iBAAiB;oCAAjB,iBAAiB;kCAAjB,iBAAiB;mCAAjB,iBAAiB;oCAAjB,iBAAiB;wCAAjB,iBAAiB;sCAAjB,iBAAiB;kCAAjB,iBAAiB;sCAAjB,iBAAiB;sCAAjB,iBAAiB;sCAAjB,iBAAiB;gCAAjB,iBAAiB;6BAAjB,iBAAiB;yBAAjB,iBAAiB;gCAAjB,iBAAiB;iCAAjB,iBAAiB;oCAAjB,iBAAiB;gCAAjB,iBAAiB;kCAAjB,iBAAiB;oCAAjB,iBAAiB;8BAAjB,iBAAiB;mCAAjB,iBAAiB;qCAAjB,iBAAiB;mCAAjB,iBAAiB;oCAAjB,iBAAiB;iCAAjB,iBAAiB;kCAAjB,iBAAiB;yBAC3pB,gBAAgB;gCAAhB,gBAAgB;4BAAhB,gBAAgB;8BAAhB,gBAAgB;+BACzB,iBAAiB;8BAAjB,iBAAiB;8BAAjB,iBAAiB;4BAC7B,iBAAiB;oCAAjB,iBAAiB"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/helpers/index.js"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;2CACyC,iCAAiC;+BAI6C,wBAAwB;2CAHjE,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;gCACrB,wBAAwB;8BAAxB,wBAAwB;gCAAxB,wBAAwB;oCAAxB,wBAAwB;wCAAxB,wBAAwB;gCAmBhB,YAAY;8BAAZ,YAAY;kCAAZ,YAAY;uCAAZ,YAAY;qCAAZ,YAAY;qCAAZ,YAAY;gCAlB3G,mBAAmB;8BAiBrB,qBAAqB;6BAEkG,aAAa;8BAAb,aAAa;yBAAb,aAAa;+BAAb,aAAa;+BAAb,aAAa;kCAAb,aAAa;+BAAb,aAAa;+BAAb,aAAa;2BAAb,aAAa;0BACT,cAAc;0BAAd,cAAc;uCAAd,cAAc;+BAAd,cAAc;sBAAd,cAAc;0BAAd,cAAc;iCAAd,cAAc;gCAAd,cAAc;sCAAd,cAAc;wCAC/H,cAAc;6BACP,cAAc;wBAAd,cAAc;wBAAd,cAAc;4BACjC,iBAAiB;+BACd,uBAAuB;mCACC,kBAAkB;mCAAlB,kBAAkB;qCAxB+I,mBAAmB;yCAAnB,mBAAmB;8BAAnB,mBAAmB;iCAAnB,mBAAmB;oCAAnB,mBAAmB;kCAAnB,mBAAmB;wCAAnB,mBAAmB;gCAAnB,mBAAmB;iCAAnB,mBAAmB;mCAAnB,mBAAmB;gCAe1I,mBAAmB;mCAAnB,mBAAmB;sCAAnB,mBAAmB;qCAAnB,mBAAmB;4BAUjC,mBAAmB;+BAAnB,mBAAmB;iCAAnB,mBAAmB;oCAAnB,mBAAmB;kCAC0mB,iBAAiB;iCAAjB,iBAAiB;gCAAjB,iBAAiB;8BAAjB,iBAAiB;+BAAjB,iBAAiB;iCAAjB,iBAAiB;8CAAjB,iBAAiB;sCAAjB,iBAAiB;uCAAjB,iBAAiB;iCAAjB,iBAAiB;oCAAjB,iBAAiB;kCAAjB,iBAAiB;mCAAjB,iBAAiB;oCAAjB,iBAAiB;wCAAjB,iBAAiB;sCAAjB,iBAAiB;kCAAjB,iBAAiB;sCAAjB,iBAAiB;sCAAjB,iBAAiB;sCAAjB,iBAAiB;gCAAjB,iBAAiB;6BAAjB,iBAAiB;yBAAjB,iBAAiB;gCAAjB,iBAAiB;iCAAjB,iBAAiB;oCAAjB,iBAAiB;gCAAjB,iBAAiB;kCAAjB,iBAAiB;oCAAjB,iBAAiB;8BAAjB,iBAAiB;mCAAjB,iBAAiB;qCAAjB,iBAAiB;mCAAjB,iBAAiB;oCAAjB,iBAAiB;iCAAjB,iBAAiB;kCAAjB,iBAAiB;yBAC3pB,gBAAgB;gCAAhB,gBAAgB;4BAAhB,gBAAgB;8BAAhB,gBAAgB;+BACzB,iBAAiB;8BAAjB,iBAAiB;8BAAjB,iBAAiB;4BAC7B,iBAAiB;oCAAjB,iBAAiB"}
@@ -30,12 +30,12 @@ import { anythingElse, goodbye, sayName } from './phrases.js';
30
30
  import { dateToWords } from './date-words.js';
31
31
  import { amountToSpeech } from './amount-to-speech.js';
32
32
  import { validateCardNumber, validateExpiration } from './card-checks.js';
33
- import { matchInputs, readFormAnswer, readInputProfile } from './match-inputs.js';
33
+ import { matchInputs, readFormAnswer, readInputProfile, formAcceptanceTrace } from './match-inputs.js';
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, rejectFormReadings, invalidateFormInputs, acceptFormReadings, formInstanceApplies, formParentResume, closeFormInstance } from './form-state.js';
35
35
  import { checkOtp, maskDestination, validateOTP, OTP_EXPIRY_MS } from './otp-check.js';
36
36
  import { validateDigits, validatePhone, validateEmail } from './validators.js';
37
37
  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, missFormInput, formInputPrompt, consumeFormFeedback, formConversationOutcome, readMoneyAmount, isMoneyAmount, moneyIntentAllows, isPastPaymentStatement, isPartialMoneyAnswer, validateMoneyOptions, takeChoiceOffer, matchesChoice, ANSWER_WORDS, CONFIRM_WORDS, isAnswer, isAgentRequest, isOtherRequest, isSpelledFragment, isAmountAnswer, amountInAnswer, pickChoice, readEmail, readPhone, readInternationalPhone, readIdentifier, last4, maskEmail, callerTextNumber, textDestination, callerIsInternational, normalizeAndFindCapture, anythingElse, goodbye, sayName, dateToWords, amountToSpeech, validateCardNumber, validateExpiration, consumeActionReceipt, authorizeFormActionRetry, formSignature, selectFormAction, confirmFormRevision, prepareFormAction, actionConfirmationValid, beginFormAction, finishFormAction, actionGateVerified, sourceNeedsLoad, beginSourceRefresh, completeSourceRefresh, pickProviderQuestion, matchInputs, readFormAnswer, readInputProfile, resolveFormInputs, formInputApplies, formInputFilled, nextFormInput, formInputChain, openFormInstance, validateFormIncompleteOutcome, formIncompleteOutcome, validateFormExhaustion, exhaustFormInput, formInitialReadings, orderFormReadings, proposedFormValues, applyFormValidation, formReadingDependencies, exclusiveFormReadings, enrichFormMatches, expandFormActivations, settleFormActivations, rememberFormArguments, resetFormInputs, hasFormValue, formPath, formValuesEqual, formReadingReady, formReadingRelevant, formPreparation, queueFormReadings, pendingFormReadings, formWorkLimit, rejectFormReadings, invalidateFormInputs, acceptFormReadings, formInstanceApplies, formParentResume, closeFormInstance, checkOtp, maskDestination, validateOTP, OTP_EXPIRY_MS, validateDigits, validatePhone, validateEmail, urlToSpeech, textWithUrlToSpeech, };
38
+ export { continuationSafetyProtocol, cancelFormTask, contextualResponseProtocol, validateFormResponse, hasFormResponse, formProposalPolicy, stageFormProposal, nextFormProposal, formProposalReadings, bindFormProposal, decideFormProposal, commitFormProposals, settleFormProposalValidation, formProposalProtocol, validateFormProposalPolicy, validateFormProposalDescriptor, formProposalDescriptor, clearFormMisses, missFormInput, formInputPrompt, consumeFormFeedback, formConversationOutcome, readMoneyAmount, isMoneyAmount, moneyIntentAllows, isPastPaymentStatement, isPartialMoneyAnswer, validateMoneyOptions, takeChoiceOffer, matchesChoice, ANSWER_WORDS, CONFIRM_WORDS, isAnswer, isAgentRequest, isOtherRequest, isSpelledFragment, isAmountAnswer, amountInAnswer, pickChoice, readEmail, readPhone, readInternationalPhone, readIdentifier, last4, maskEmail, callerTextNumber, textDestination, callerIsInternational, normalizeAndFindCapture, anythingElse, goodbye, sayName, dateToWords, amountToSpeech, validateCardNumber, validateExpiration, consumeActionReceipt, authorizeFormActionRetry, formSignature, selectFormAction, confirmFormRevision, prepareFormAction, actionConfirmationValid, beginFormAction, finishFormAction, actionGateVerified, sourceNeedsLoad, beginSourceRefresh, completeSourceRefresh, pickProviderQuestion, matchInputs, readFormAnswer, readInputProfile, 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, rejectFormReadings, invalidateFormInputs, acceptFormReadings, formInstanceApplies, formParentResume, closeFormInstance, checkOtp, maskDestination, validateOTP, OTP_EXPIRY_MS, validateDigits, validatePhone, validateEmail, urlToSpeech, textWithUrlToSpeech, };
39
39
  /** The functions flows call by name, ready to spread into a host's approved functions. */
40
40
  export const STANDARD_FUNCTIONS = {
41
41
  continuationSafetyProtocol, cancelFormTask, contextualResponseProtocol, validateFormResponse, hasFormResponse,
@@ -43,6 +43,6 @@ export const STANDARD_FUNCTIONS = {
43
43
  clearFormMisses, missFormInput, formInputPrompt, consumeFormFeedback, formConversationOutcome,
44
44
  readMoneyAmount, isMoneyAmount, moneyIntentAllows, isPastPaymentStatement, isPartialMoneyAnswer, validateMoneyOptions, takeChoiceOffer, matchesChoice, isAnswer, isAgentRequest, isOtherRequest, isSpelledFragment, isAmountAnswer, amountInAnswer, pickChoice,
45
45
  readEmail, readPhone, readInternationalPhone, readIdentifier, last4, maskEmail, callerTextNumber, textDestination, callerIsInternational,
46
- normalizeAndFindCapture, anythingElse, goodbye, sayName, dateToWords, amountToSpeech, validateCardNumber, validateExpiration, consumeActionReceipt, authorizeFormActionRetry, formSignature, selectFormAction, confirmFormRevision, prepareFormAction, actionConfirmationValid, beginFormAction, finishFormAction, actionGateVerified, sourceNeedsLoad, beginSourceRefresh, completeSourceRefresh, pickProviderQuestion, matchInputs, readFormAnswer, readInputProfile, resolveFormInputs, formInputApplies, formInputFilled, nextFormInput, formInputChain, openFormInstance, validateFormIncompleteOutcome, formIncompleteOutcome, validateFormExhaustion, exhaustFormInput, formInitialReadings, orderFormReadings, proposedFormValues, applyFormValidation, formReadingDependencies, exclusiveFormReadings, enrichFormMatches, expandFormActivations, settleFormActivations, rememberFormArguments, resetFormInputs, hasFormValue, formPath, formValuesEqual, formReadingReady, formReadingRelevant, formPreparation, queueFormReadings, pendingFormReadings, formWorkLimit, rejectFormReadings, invalidateFormInputs, acceptFormReadings, formInstanceApplies, formParentResume, closeFormInstance,
46
+ normalizeAndFindCapture, anythingElse, goodbye, sayName, dateToWords, amountToSpeech, validateCardNumber, validateExpiration, consumeActionReceipt, authorizeFormActionRetry, formSignature, selectFormAction, confirmFormRevision, prepareFormAction, actionConfirmationValid, beginFormAction, finishFormAction, actionGateVerified, sourceNeedsLoad, beginSourceRefresh, completeSourceRefresh, pickProviderQuestion, matchInputs, readFormAnswer, readInputProfile, 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, rejectFormReadings, invalidateFormInputs, acceptFormReadings, formInstanceApplies, formParentResume, closeFormInstance,
47
47
  validateOTP, validateDigits, validatePhone, validateEmail, urlToSpeech, textWithUrlToSpeech,
48
48
  };
@@ -1,3 +1,9 @@
1
+ /**
2
+ * This turn's acceptance decisions for the host to log, appended per match: fields filled out of order, in a compound
3
+ * answer or as an invited input, and every refused reading, by form, field and rule. Field names and rules only, never
4
+ * values: the answers include card numbers and codes. The host clears it at the start of a turn.
5
+ */
6
+ export function formAcceptanceTrace(previous: any, matches: any): any;
1
7
  /**
2
8
  * One input's reading of the answer: { value, evidence } or null. `evidence` is what the answer spent on it (digits
3
9
  * or words), so two inputs that claim the same part of the answer are told apart from a compound answer.
@@ -31,8 +37,40 @@ export function readFormAnswer(answer: any, candidates: any, current: any, choic
31
37
  ambiguous: any;
32
38
  remaining: any[];
33
39
  questions: any[];
40
+ decisions?: {
41
+ form: any;
42
+ name: any;
43
+ rule: any;
44
+ }[] | undefined;
45
+ refused?: ({
46
+ form: any;
47
+ name: any;
48
+ rule: string;
49
+ value?: undefined;
50
+ } | {
51
+ form: any;
52
+ name: any;
53
+ value: any;
54
+ rule: string;
55
+ })[] | undefined;
34
56
  };
35
57
  export function matchInputs(answer: any, candidates: any, current: any, choiceKey: any): {
58
+ decisions?: {
59
+ form: any;
60
+ name: any;
61
+ rule: any;
62
+ }[] | undefined;
63
+ refused?: ({
64
+ form: any;
65
+ name: any;
66
+ rule: string;
67
+ value?: undefined;
68
+ } | {
69
+ form: any;
70
+ name: any;
71
+ value: any;
72
+ rule: string;
73
+ })[] | undefined;
36
74
  remaining?: any[] | undefined;
37
75
  hits: {
38
76
  [x: string]: any;
@@ -1 +1 @@
1
- {"version":3,"file":"match-inputs.d.ts","sourceRoot":"","sources":["../../src/helpers/match-inputs.js"],"names":[],"mappings":"AAoFA;;;GAGG;AACH,sFAgGC;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH;mHACmH;AACnH;;;;;EAeC;AACD;;;;;;EAkHC"}
1
+ {"version":3,"file":"match-inputs.d.ts","sourceRoot":"","sources":["../../src/helpers/match-inputs.js"],"names":[],"mappings":"AAiBA;;;;GAIG;AACH,sEAQC;AAqED;;;GAGG;AACH,sFAwGC;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH;mHACmH;AACnH;;;;;;;;;;;;;;;;;;;;;EAeC;AACD;;;;;;;;;;;;;;;;;;;;;;EA0KC"}
@@ -9,11 +9,26 @@
9
9
  * the flows. Part of jsfe/helpers; imports only siblings.
10
10
  */
11
11
  import { matchesChoice } from './matches-choice.js';
12
- import { pickChoice, amountInAnswer, isAmountAnswer } from './answer.js';
12
+ import { pickChoice, amountInAnswer, isAmountAnswer, ANSWER_WORDS, CONFIRM_WORDS } from './answer.js';
13
13
  import { readEmail, readPhone, readInternationalPhone, readIdentifier } from './contact.js';
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
+ /**
18
+ * This turn's acceptance decisions for the host to log, appended per match: fields filled out of order, in a compound
19
+ * answer or as an invited input, and every refused reading, by form, field and rule. Field names and rules only, never
20
+ * values: the answers include card numbers and codes. The host clears it at the start of a turn.
21
+ */
22
+ export function formAcceptanceTrace(previous, matches) {
23
+ const filled = (matches?.decisions || []).map((d) => ({ form: String(d.form).replace(/:\d+$/, ''), name: d.name, rule: d.rule }));
24
+ const refused = (matches?.refused || []).map((r) => ({ form: String(r.form).replace(/:\d+$/, ''), name: r.name, rule: r.rule }));
25
+ if (!filled.length && !refused.length) {
26
+ return previous ?? null;
27
+ }
28
+ else {
29
+ return { filled: [...(previous?.filled || []), ...filled], refused: [...(previous?.refused || []), ...refused] };
30
+ }
31
+ }
17
32
  /** The answer as said: jsfe stores user input escaped (don\'t). */
18
33
  function unescape(answer) {
19
34
  return String(answer ?? '').replace(/\\(['"\\])/g, '$1');
@@ -91,6 +106,15 @@ function unclaimedSpans(raw, readings) {
91
106
  */
92
107
  export function readInputProfile(profile, answer, isCurrent = true) {
93
108
  const raw = unescape(answer);
109
+ // A field may declare a narrower signature for out-of-order reading: the payment card takes a bare
110
+ // last four when asked, but out of order only a card word or "ending in 4242".
111
+ if (!isCurrent && profile && profile.out_of_order && typeof profile.out_of_order === 'object') {
112
+ const { out_of_order, ...asked } = profile;
113
+ return readInputProfile({ ...asked, ...out_of_order }, answer, isCurrent);
114
+ }
115
+ else {
116
+ // asked, or no out-of-order signature
117
+ }
94
118
  const kind = profile && profile.kind;
95
119
  if (kind === 'choice') {
96
120
  const choices = (Array.isArray(profile.choices) ? profile.choices : []).filter(c => isCurrent || c.current_only !== true).map(c => ({ ...c,
@@ -271,9 +295,23 @@ export function matchInputs(answer, candidates, current, choiceKey) {
271
295
  forms.push(c.scope || c.form);
272
296
  }
273
297
  else { /* already ranked */ } });
298
+ const fieldOf = (reading) => list.find((c) => c.form === reading.form && c.name === reading.name) || { profile: {} };
299
+ const spanText = (reading) => (reading.spans || []).map(([a, b]) => raw.slice(a, b)).join(' ');
300
+ const refused = [];
274
301
  let readings = [];
275
302
  for (const c of list) {
276
- const profile = narrativeCurrent && c.profile.kind === 'email' ? { ...c.profile, literal: true } : c.profile;
303
+ // Rule 2: a confirmation is answered only when asked, firm by CONFIRM_WORDS, soft by the wider YES vocabulary.
304
+ const confirmation = ['firm', 'soft'].includes(c.confirmation) ? c.confirmation : '';
305
+ if (confirmation && !isCur(c)) {
306
+ if (readInputProfile(c.profile, raw, false))
307
+ refused.push({ form: c.form, name: c.name, rule: 'confirmation_out_of_order' });
308
+ continue;
309
+ }
310
+ else {
311
+ // asked, or not a confirmation
312
+ }
313
+ const vocabulary = confirmation ? { ...c.profile, choices: (c.profile.choices || []).map((x) => x.key === 'yes' ? { ...x, phrases: [...(x.phrases ?? x.words ?? []), ...(confirmation === 'firm' ? CONFIRM_WORDS : ANSWER_WORDS.yes)], exact: confirmation === 'firm' || x.exact === true } : x.key === 'no' ? { ...x, phrases: [...(x.phrases ?? x.words ?? []), ...ANSWER_WORDS.no] } : x) } : c.profile;
314
+ const profile = narrativeCurrent && vocabulary.kind === 'email' ? { ...vocabulary, literal: true } : vocabulary;
277
315
  let r = readInputProfile(profile, raw, isCur(c));
278
316
  // A semantic choice supplied by ask-choice (e.g. spoken keypad keys) is a reading, not an early return.
279
317
  if (!r && c === own && choiceKey && c.profile.kind === 'choice' && (c.profile.choices || []).some(x => x.key === choiceKey)) {
@@ -293,6 +331,42 @@ export function matchInputs(answer, candidates, current, choiceKey) {
293
331
  }
294
332
  }
295
333
  }
334
+ // Context-aware acceptance (docs/copilot-reference.md §5; owner 2026-10-25): the standard rules for every form.
335
+ // The inputs the current question also asks for: its declared invites, or a read-back's (current names no input). An
336
+ // invite names an input of the asking form or of a form enclosing it; the nearest such form wins by scope.
337
+ const invitedNames = new Set([...list.filter(isCur).flatMap((c) => Array.isArray(c.invites) ? c.invites : []), ...(Array.isArray(cur.invites) ? cur.invites : [])]);
338
+ const invited = (reading) => !reading.current && invitedNames.has(reading.name);
339
+ // A declared confirm_against pair with the current question is relevant to it as well: the pair is asked about.
340
+ const pairedWithCurrent = (reading) => list.some((c) => isCur(c) && c.form === reading.form && ((c.confirm_against || []).includes(reading.name) || (fieldOf(reading).confirm_against || []).includes(c.name)));
341
+ const inOrder = (reading) => reading.current || invited(reading) || pairedWithCurrent(reading);
342
+ // Rule 3: a number read out of order must be whole: every digit run its evidence touches lies entirely inside it,
343
+ // so the last four of "88 91 7112" fill nothing.
344
+ const runs = [...raw.matchAll(/\d(?:[\s.-]?\d)*/g)].map((m) => [m.index, m.index + m[0].length]);
345
+ const wholeRuns = (reading) => runs.every(([a, b]) => !(reading.spans || []).some(([s, e]) => s < b && a < e) || (reading.spans || []).some(([s, e]) => s <= a && e >= b));
346
+ // Rule 4: a bare number has no money mark and no word in its evidence.
347
+ const bare = (reading) => !isAmountAnswer(raw) && (reading.spans || []).length > 0 && (reading.spans || []).every(([a, b]) => /\d/.test(raw.slice(a, b)) && !/\p{L}/u.test(raw.slice(a, b)));
348
+ readings = readings.filter((reading) => {
349
+ if (inOrder(reading)) {
350
+ if (!reading.current)
351
+ reading.rule = 'invited'; // the current input is the default: no rule recorded
352
+ else { /* the current input */ }
353
+ return true;
354
+ }
355
+ else if (bare(reading) && fieldOf(reading).profile.kind === 'amount') {
356
+ refused.push({ form: reading.form, name: reading.name, value: reading.value, rule: 'bare_amount_out_of_order' });
357
+ return false;
358
+ }
359
+ else if (/\d/.test(spanText(reading)) && !wholeRuns(reading)) {
360
+ refused.push({ form: reading.form, name: reading.name, value: reading.value, rule: 'fragment' });
361
+ return false;
362
+ }
363
+ else {
364
+ reading.rule = 'out_of_order';
365
+ return true;
366
+ }
367
+ });
368
+ // Rule 4: a single digit the current question offers as a key is that key (the keypad reading wins its group below);
369
+ // one it does not offer reaches the invited amount, whose own validation decides ("3 dollars?").
296
370
  const paired = (a, b) => a.confirm.indexOf(b.name) > -1 || b.confirm.indexOf(a.name) > -1;
297
371
  // Narrative ownership protects ancestor choices and same-form choices that reset
298
372
  // the requested field. Other sibling choices remain independent compound data. A current choice can carry narrative context too (e.g. a channel plus a
@@ -342,6 +416,7 @@ export function matchInputs(answer, candidates, current, choiceKey) {
342
416
  effective.forEach((r, i) => { (groups[find(i)] = groups[find(i)] || []).push(r); });
343
417
  const strip = ({ evidence, spans, keypad, fallback, repeatedChoice, wholeChoice, choice, narrative, resetOn, level, group, exclusive, current: c, confirm, ...r }) => r;
344
418
  const hits = [];
419
+ const taken = [];
345
420
  let ambiguous = [];
346
421
  const remaining = [];
347
422
  for (const group of Object.values(groups)) {
@@ -365,7 +440,7 @@ export function matchInputs(answer, candidates, current, choiceKey) {
365
440
  const confirmWith = winners.length === 1 ? members.filter((m) => m !== winners[0] && paired(m, winners[0])) : [];
366
441
  const asked = confirmWith.length ? winners.concat(confirmWith) : winners;
367
442
  if (asked.length === 1) {
368
- hits.push(strip(asked[0]));
443
+ taken.push(asked[0]);
369
444
  }
370
445
  else if (ambiguous.length === 0) {
371
446
  ambiguous = asked.map(strip);
@@ -374,5 +449,18 @@ export function matchInputs(answer, candidates, current, choiceKey) {
374
449
  remaining.push(asked.map(strip));
375
450
  }
376
451
  }
377
- return { hits, ambiguous, ...(remaining.length ? { remaining } : {}) };
452
+ // Rule 5: one part of an answer never fills two fields. A reading that shares evidence with another taken reading is
453
+ // refused with it, never kept by order.
454
+ const clash = (a, b) => (a.spans || []).some(([s, e]) => (b.spans || []).some(([t, u]) => s < u && t < e));
455
+ const kept = taken.filter((a) => !taken.some((b) => b !== a && clash(a, b)));
456
+ for (const r of taken.filter((a) => !kept.includes(a)))
457
+ refused.push({ form: r.form, name: r.name, value: r.value, rule: 'one_part_two_fields' });
458
+ // Several fields from one answer: those filled out of order are a compound answer.
459
+ if (kept.length > 1)
460
+ kept.forEach((r) => { if (r.rule === 'out_of_order')
461
+ r.rule = 'compound'; });
462
+ hits.push(...kept.map(({ rule, ...r }) => strip(r)));
463
+ // Why each field was filled other than as the current answer, kept beside the hits so their shape never changes.
464
+ const decisions = kept.filter((r) => r.rule).map((r) => ({ form: r.form, name: r.name, rule: r.rule }));
465
+ return { hits, ambiguous, ...(remaining.length ? { remaining } : {}), ...(refused.length ? { refused } : {}), ...(decisions.length ? { decisions } : {}) };
378
466
  }
@@ -1 +1 @@
1
- export const JSFE_RUNTIME_FINGERPRINT = "sha256:7a048ac938debc2bb9dcd648489cde344d9eba57521231cabe6c2b49f36251bd";
1
+ export const JSFE_RUNTIME_FINGERPRINT = "sha256:6f3a7201cfa214cba9a28fb392f8d61c4d731287b90a56b89cf191968d9edf56";
@@ -194,6 +194,44 @@ Matching depends on policy. With `exact:true`, a phrase must form the whole norm
194
194
 
195
195
  JSFE resolves valid current-field evidence first, then a unique match in the nearest owning form, then ancestors; genuine competing claims are clarified. Independent spans can fill independent fields. Typed evidence outranks arbitrary text. `confirm_against` deliberately retains ambiguity. A child result uses the same acceptance/invalidation transition as a leaf. Pending dependent candidates wait for valid prerequisites; invalid data cannot authorize an action. Narrative mode does not turn fragments or bare structured identifiers into business explanations.
196
196
 
197
+ ### Context-aware acceptance
198
+
199
+ A caller answers out of order, often several fields at once. That ability is kept, and every out-of-order answer is
200
+ validated against its context: an answer fills another question's field only where it is relevant there. These are the
201
+ standard rules for every form; the declarations below tell the engine what each field is.
202
+
203
+ Declarations:
204
+
205
+ | Declaration | Meaning |
206
+ | --- | --- |
207
+ | 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. |
208
+ | 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. |
209
+ | 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". |
210
+ | input `choices_from` | The choice's values come from prepared data (the caller's saved cards), so its signature is those values. |
211
+ | `form.compound:"worker"` | A parser that reads several of the form's fields from one answer (an address said in one breath). |
212
+
213
+ Rules, in order:
214
+
215
+ 1. **The current question first.** The whole answer is read against the current input's signature and the inputs it
216
+ invites. A match is that answer.
217
+ 2. **A confirmation is answered only when it is asked.** Out of order it never makes sense, so no words, YES, NO or the
218
+ option's own phrases, fill a confirmation that is not the current question.
219
+ 3. **Out of order requires the whole signature.** A digit field takes a number only when the whole number fits its
220
+ length; a pattern only when it covers the whole number; a choice only its declared or prepared values. A fragment of
221
+ a longer number (the last four of "88 91 7112") fills nothing.
222
+ 4. **A bare number** (no money mark, no word naming a field) is an amount only at a question that is, invites or is
223
+ declared `confirm_against` an amount. A single digit the question offers as a key is that key; one it does not offer reaches the invited amount,
224
+ whose validation decides (Curacao asks for it in dollars). A bare number that does not fit the current question is
225
+ read out of order under rule 3.
226
+ 5. **Compound answers.** Several fields fill from one answer only through the form's compound parser, or when every
227
+ part of the answer fits exactly one field. **One part of an answer never fills two fields**; the engine refuses that
228
+ reading and records the refusal.
229
+ 6. **Scope.** Across forms the nearest form wins; two fields of the same form that fit the same part are asked about,
230
+ never ranked.
231
+
232
+ Every result records why: `decisions` lists each field filled other than as the current answer with its rule (`invited`, `out_of_order`, `compound`), and every
233
+ refused reading is listed with its rule, so hosts can log what was filled out of order and what was refused.
234
+
197
235
  ## 6. Validation, reentry and contextual uncertainty
198
236
 
199
237
  A field validator receives `{form,input,value,values,reading}` and writes `cargo.inputCheck={ok:true,value?}` or `{ok:false,message?,message_es?}`. Missing/malformed results fail closed. Activation values and conversational answers use the same validation path. `value` may normalize a valid input; never return success for a refusal to satisfy an effect's prerequisites.
@@ -1,3 +1,20 @@
1
+ ## 1.10.0: context-aware acceptance
2
+
3
+ An answer given out of order fills another question's field only where it is relevant there (reference §5): a confirmation
4
+ (`confirmation:"firm"` or `"soft"` on a choice keyed yes and no) is answered only when asked, firm by the confirm words and
5
+ soft by the wider YES vocabulary; a bare number is an amount only at a question that is, invites (`invites`, also on a
6
+ read-back's `confirm`) or is paired (`confirm_against`) with one; a number read out of order must be whole, never the
7
+ fragment of a longer one; a profile may declare a narrower out-of-order signature (`out_of_order`); and one part of an
8
+ answer never fills two fields. `matchInputs` lists `refused` readings and the `decisions` behind out-of-order fills, and
9
+ the form workers append them to `cargo.formAcceptance` (`formAcceptanceTrace`, names and rules only) for the host to log.
10
+ At the code question, "Otra", "Número de cuenta" and "Account number" mean ANOTHER ACCOUNT.
11
+
12
+ ## 1.9.4: card groups start like a card
13
+
14
+ `accumulate_first` (a regular expression) limits accumulation to a first group that matches it; the `card_number` preset
15
+ uses `^[2-6]` (every card network's first digit), so "1234" or "0729" said at the card question keeps its normal reading
16
+ as an expiration date or a security code.
17
+
1
18
  ## 1.9.3: a card number said in groups
2
19
 
3
20
  A digit input may opt in to `accumulate: true`: an answer that is only digits and short of the input's minimum is kept, and
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jsfe",
3
- "version": "1.9.3",
3
+ "version": "1.10.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",