jsfe 1.11.1 → 1.13.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.
@@ -228,6 +228,7 @@ export function validateFlowContracts(flows: any, tools?: any[], { externalFlows
228
228
  max: number;
229
229
  };
230
230
  pii: boolean;
231
+ validator: string;
231
232
  question: string;
232
233
  question_es: string;
233
234
  };
@@ -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"}
@@ -365,6 +365,8 @@ export namespace FIELD_PRESETS {
365
365
  export { digits_6 as digits };
366
366
  let pii_2: boolean;
367
367
  export { pii_2 as pii };
368
+ let validator_2: string;
369
+ export { validator_2 as validator };
368
370
  let question_11: string;
369
371
  export { question_11 as question };
370
372
  let question_es_11: string;
@@ -1 +1 @@
1
- {"version":3,"file":"flows.d.ts","sourceRoot":"","sources":["../../src/forms/flows.js"],"names":[],"mappings":"AA4HA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA4DC;AACD;;;GAGG;AACH;;;;;;EAGC;AAmbD,wHAAwH;AACxH,wEAaC;AACD,4FAA4F;AAC5F;;;EAYC;AACD,sGAAsG;AACtG;;;EAOC;AA9mBM,6CAAuG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsyB9G;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAOG"}
1
+ {"version":3,"file":"flows.d.ts","sourceRoot":"","sources":["../../src/forms/flows.js"],"names":[],"mappings":"AA6HA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA4DC;AACD;;;GAGG;AACH;;;;;;EAGC;AAocD,wHAAwH;AACxH,wEAaC;AACD,4FAA4F;AAC5F;;;EAYC;AACD,sGAAsG;AACtG;;;EAOC;AAhoBM,6CAAuG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAyzB9G;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAOG"}
@@ -82,6 +82,7 @@ export const FIELD_PRESETS = {
82
82
  retry: 'Please say or enter the expiration as four digits, month then year, like 0 9 2 8.', retry_es: 'Por favor diga o ingrese el vencimiento en cuatro dígitos, mes y año, por ejemplo 0 9 2 8.',
83
83
  retry_text: 'Please type the expiration as four digits, month then year, like 0928.', retry_text_es: 'Por favor escriba el vencimiento en cuatro dígitos, mes y año, por ejemplo 0928.' },
84
84
  card_cvv: { label: 'security code', label_es: 'código de seguridad', profile: { kind: 'digits', min: 3, max: 4 }, digits: { min: 3, max: 4 }, pii: true,
85
+ validator: 'validate-card-cvv',
85
86
  question: 'What is the security code on the back of the card?', question_es: '¿Cuál es el código de seguridad al reverso de la tarjeta?' },
86
87
  card_zip: { label: 'billing ZIP code', label_es: 'código postal de facturación', profile: { kind: 'digits', min: 5, max: 5 }, digits: { min: 5, max: 5 },
87
88
  question: 'What is the billing ZIP code for the card?', question_es: '¿Cuál es el código postal de facturación de la tarjeta?' },
@@ -411,6 +412,17 @@ const readFormChoice = {
411
412
  SIDE('choice_clear', 'clearFormMisses(cargo,form,input)'),
412
413
  FLOW('choice_accept', 'collect-input-accept', 'call', { hits: '{{reading.hits}}' })]
413
414
  };
415
+ // A confirmation's unread answer goes to the reply ranker with the confirmation question (owner 2026-10-27), never
416
+ // straight to the host. The ranker is offered NO only: a confirmation is accepted only by its exact YES. Result:
417
+ // cargo.confirmRanked = 'no' | 'none' (ConfirmForm reads it); `abort` ends every flow; `new_request` goes to intent detection.
418
+ const readConfirmReply = {
419
+ id: 'read-confirm-reply', name: 'ReadConfirmReply', version: '1.0.0', scope: 'isolated', description: 'Rank an unreadable answer to a confirmation: it declines (NO) or is asked again; a withdrawal or a new request leaves with saved form progress. Never confirms.',
420
+ parameters: [{ name: 'form', type: 'string' }, { name: 'question', type: 'string' }, { name: 'question_es', type: 'string' }],
421
+ variables: vars({ question: '', question_es: '', asked: '', reply: null }),
422
+ steps: [SET('confirm_asked', 'asked', "language === 'es' && question_es ? question_es : question"),
423
+ { ...yieldDispatch('confirm_dispatch', 'form', "'confirmation_not_matched'"), onResponse: 'continue', responseContext: 'Current question: {{asked}}', responseParameters: [{ name: 'key', type: 'string', description: "'no' when the reply declines or refuses what the question asks to confirm. Leave it empty otherwise, including when it agrees." }], responseVariable: 'reply' },
424
+ SIDE('confirm_ranked', "(cargo.confirmRanked=String(reply && reply.key || '').trim().toLowerCase()==='no' ? 'no' : 'none',true)")]
425
+ };
414
426
  const collectContextualAnswer = formFlow({ id: 'collect-contextual-answer', name: 'CollectContextualAnswer', description: 'Collect one open-ended answer using contextual DISPATCH and preserve the caller wording.',
415
427
  parameters: [...['question', 'question_es', 'description', 'context', 'context_es'].map(name => ({ name, type: 'string' })), { name: 'choices', type: 'array' }, { name: 'digits', type: 'object' }],
416
428
  extraVars: { question: '', question_es: '', description: '', context: 'your request', context_es: 'su solicitud', choices: [], digits: null },
@@ -495,11 +507,11 @@ const disambiguate = {
495
507
  // ── confirm-form: the exact confirmation of a finished form; a correction is matched against the form chain ───────
496
508
  const confirmForm = {
497
509
  id: 'confirm-form', name: 'ConfirmForm', version: '1.0.0', scope: 'isolated',
498
- 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).",
510
+ 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 unread one goes to the reply ranker (read-confirm-reply). Result: cargo.formConfirm[form] = 'yes' | 'no' | 'changed'. Params: form, question(_es), exit_context(_es).",
499
511
  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" }]),
500
512
  variables: vars({ named_fields: false, cf_must: 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: '' }),
501
513
  steps: [
502
- SIDE('cf_reset', "(cargo.formConfirm = cargo.formConfirm || {}, cargo.formConfirm[form] = '', true)"),
514
+ SIDE('cf_reset', "(cargo.formConfirm = cargo.formConfirm || {}, cargo.formConfirm[form] = '', cargo.confirmRanked = '', true)"),
503
515
  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};}))`),
504
516
  SET('cf_initial_chain', 'cf_chain', CHAIN_JS),
505
517
  SET('cf_amount_context', 'cf_amount', "cf_chain.some(function(input){return input.profile && input.profile.kind==='amount';})"),
@@ -534,9 +546,11 @@ const confirmForm = {
534
546
  'condition: cf_named': SET('cf_named_reset', 'cf_result', "(resetFormInputs(cargo, cf_named.form, [cf_named.name]), 'changed')"),
535
547
  "condition: cargo.choice === 'no'": SET('cf_declined', 'cf_result', "'no'"),
536
548
  "condition: Array.isArray(clarifications) && pickChoice(String(cargo.choiceText || '').replace(/[?¿]/g,''),[{key:'clarify',phrases:clarifications,exact:true}],false)==='clarify'": SET('cf_clarification', 'cf_result', "'clarification'"),
537
- 'condition: !cf_must && (cf_words >= 3 || cf_amount && cf_words > 0)': yieldDispatch('cf_elsewhere', 'form', "'confirmation_not_matched'"),
549
+ 'condition: !cf_must && (cf_words >= 3 || cf_amount && cf_words > 0)': FLOW('cf_elsewhere', 'read-confirm-reply', 'call', { form: '{{form}}', question: '{{cf_prompt}}', question_es: '{{cf_prompt_es}}' }),
538
550
  default: SET('cf_unclear', 'cf_result', "'unclear'")
539
551
  } },
552
+ // the ranker's reading of an unread answer: a decline is the NO; an answer that declines nothing is asked again
553
+ SET('cf_ranked', 'cf_result', "cf_result ? cf_result : cargo.confirmRanked === 'no' ? 'no' : cargo.confirmRanked === 'none' ? 'unclear' : ''"),
540
554
  SET('cf_changed', 'cf_result', "cf_result ? cf_result : 'changed'"),
541
555
  // an unclear short answer is asked again (the form runs again and reaches this question), at most twice
542
556
  SET('cf_tries_count', 'cf_tries', "cf_result === 'unclear' ? missFormInput(cargo,form,'__confirm') : (clearFormMisses(cargo,form,'__confirm'),0)"),
@@ -589,6 +603,10 @@ const validators = [
589
603
  description: 'Validator: month and year, a real month, not expired (validateExpiration). Normalizes to MMYY. Params: input, value.',
590
604
  parameters: [{ name: 'input', type: 'string', description: 'input name' }, { name: 'value', type: 'string', description: 'the reading' }],
591
605
  steps: [SIDE('vce_check', "(function (v) { var m = /^(0?[1-9]|1[0-2])\\D*(?:20)?(\\d{2})$/.exec(String(v).replace(/\\s+/g, '').replace(/de/gi, '')) || /^(\\d{2})(\\d{2})$/.exec(String(v).replace(/\\D/g, '')) || /^(0[1-9])0(\\d{2})$/.exec(String(v).replace(/\\D/g, '')); var mm = m ? ('0' + m[1]).slice(-2) : ''; var yy = m ? m[2] : ''; cargo.inputCheck = m && validateExpiration(mm, yy) ? { ok: true, value: mm + yy } : { ok: false, message: \"That expiration date doesn't look right.\", message_es: 'Esa fecha de vencimiento no parece correcta.' }; return true; })(value)")] },
606
+ { id: 'validate-card-cvv', name: 'ValidateCardCvv', version: '1.0.0', scope: 'isolated',
607
+ description: 'Validator: the security code has the length of its card brand, 4 digits for American Express (34, 37) and 3 for every other brand; with no card number yet, 3 or 4. Params: input, value, values.',
608
+ parameters: [{ name: 'input', type: 'string', description: 'input name' }, { name: 'value', type: 'string', description: 'the reading' }, { name: 'values', type: 'object', description: "the form's values, for the card number" }],
609
+ steps: [SIDE('vcc_check', "(function (v, n) { var d = String(v).replace(/\\D/g, ''); var pan = String(n || '').replace(/\\D/g, ''); var want = pan ? (/^3[47]/.test(pan) ? 4 : 3) : 0; var ok = want ? d.length === want : d.length === 3 || d.length === 4; cargo.inputCheck = ok ? { ok: true, value: d } : want === 4 ? { ok: false, message: \"That security code doesn't look right. It is the 4 digits on the front of the card.\", message_es: 'Ese código de seguridad no parece correcto. Son los 4 dígitos al frente de la tarjeta.' } : { ok: false, message: \"That security code doesn't look right. It is the 3 digits on the back of the card.\", message_es: 'Ese código de seguridad no parece correcto. Son los 3 dígitos al reverso de la tarjeta.' }; return true; })(value, typeof values !== 'undefined' && values ? values.card_number : '')")] },
592
610
  ];
593
611
  // ── field collectors and sub-forms ──────────────────────────────────────────────────────────────────────────────────
594
612
  const fieldCollectors = Object.entries(FIELD_COLLECTORS).map(([id, field]) => formFlow({
@@ -826,7 +844,7 @@ const resumeForm = { id: 'resume-form', name: 'ResumeForm', version: '1.0.0', sc
826
844
  SET('resume_definition', 'target', 'cargo.forms[form].definition'), SET('resume_parent', 'parent', "cargo.forms[form].parent || ''"), SET('resume_slot', 'slot', "cargo.forms[form].result_input || ''"),
827
845
  FLOW('resume_owned_form', '{{target}}', 'replace', { form_instance: '{{form}}', parent_form: '{{parent}}', result_input: '{{slot}}' })
828
846
  ] };
829
- const LIBRARY_FLOWS = [readFormReply, readFormChoice, confirmInputProposal, proposalDeclined, resumeForm, performAction, sendTextAction, prepareInput, runForm, collectInput, validateBatch, requestCancelled, readFormResponse, collectContextualAnswer, accept, acceptedEffects, disambiguate, confirmForm, formDone, providerMenu, ...validators, ...fieldCollectors, ...subForms, provideData, provideQueuedData, loadSource, answerFacts];
847
+ const LIBRARY_FLOWS = [readFormReply, readFormChoice, readConfirmReply, confirmInputProposal, proposalDeclined, resumeForm, performAction, sendTextAction, prepareInput, runForm, collectInput, validateBatch, requestCancelled, readFormResponse, collectContextualAnswer, accept, acceptedEffects, disambiguate, confirmForm, formDone, providerMenu, ...validators, ...fieldCollectors, ...subForms, provideData, provideQueuedData, loadSource, answerFacts];
830
848
  const LIBRARY_CONTRACTS = {
831
849
  'confirm-input-proposal': { role: 'collector', requires: [INPUT_PROPOSALS_REQUIREMENT] }, 'confirm-input-proposal-declined': { role: 'worker', requires: [INPUT_PROPOSALS_REQUIREMENT] },
832
850
  'resume-form': { role: 'worker' },
@@ -835,7 +853,7 @@ const LIBRARY_CONTRACTS = {
835
853
  'prepare-input': { role: 'worker' }, 'run-form': { role: 'worker' }, 'collect-input': { role: 'worker' },
836
854
  'request-cancelled': { role: 'worker' },
837
855
  'read-form-response': { role: 'worker', requires: [CONTEXTUAL_RESPONSES_REQUIREMENT] },
838
- 'read-form-reply': { role: 'worker' }, 'read-form-choice': { role: 'worker', requires: [CONTEXTUAL_RESPONSES_REQUIREMENT] },
856
+ 'read-form-reply': { role: 'worker' }, 'read-form-choice': { role: 'worker', requires: [CONTEXTUAL_RESPONSES_REQUIREMENT] }, 'read-confirm-reply': { role: 'worker', requires: [CONTEXTUAL_RESPONSES_REQUIREMENT] },
839
857
  'collect-contextual-answer': { role: 'collector', requires: [CONTEXTUAL_RESPONSES_REQUIREMENT] },
840
858
  'validate-input-batch': { role: 'worker', result: { path: 'cargo.inputBatch', type: 'object' } },
841
859
  'collect-input-accept': { role: 'worker' }, 'input-accepted-effects': { role: 'worker' },
@@ -844,6 +862,7 @@ const LIBRARY_CONTRACTS = {
844
862
  'load-source': { role: 'worker' }, 'answer-facts': { role: 'worker' },
845
863
  'validate-card-number': { role: 'worker', result: { path: 'cargo.inputCheck', type: 'object' } },
846
864
  'validate-card-expiration': { role: 'worker', result: { path: 'cargo.inputCheck', type: 'object' } },
865
+ 'validate-card-cvv': { role: 'worker', result: { path: 'cargo.inputCheck', type: 'object' } },
847
866
  'collect-phone': { role: 'collector' }, 'collect-email': { role: 'collector' }, 'collect-zip': { role: 'collector' },
848
867
  'collect-street-address': { role: 'collector' }, 'collect-city': { role: 'collector' }, 'collect-card-number': { role: 'collector' },
849
868
  'collect-card-expiration': { role: 'collector' }, 'collect-card-cvv': { role: 'collector' }, 'collect-card-zip': { role: 'collector' },
@@ -347,8 +347,8 @@ export function numericAmountReading(input) {
347
347
  // Mixed answers with an explicit monetary marker or payment verb still parse their independent amount.
348
348
  if (/\b(?:cards?|tarjetas?|ending|termina(?:d[ao]s?)?|visa|mastercard|amex|discover)\b/i.test(raw) && !isAmountAnswer(raw) && !/\b(?:pay|paying|pagar|abonar)(?:\b|(?=\d))/i.test(raw))
349
349
  return { value: NaN, candidates: 0 };
350
- // 1.234,56 (European) → 1234.56; 1,234.56 → 1234.56; 12,50 → 12.50
351
- let t = /\d{1,3}(\.\d{3})+,\d{1,2}\b/.test(raw) ? raw.replace(/\./g, '').replace(',', '.') : raw.replace(/,(?=\d{3}\b)/g, '').replace(/(\d),(\d{1,2})\b/g, '$1.$2');
350
+ // 1.234,56 (European) → 1234.56; 1,234.56 → 1234.56; 12,50 → 12.50; 1.500 → 1500 (three digits after a dot are never cents)
351
+ let t = /\d{1,3}(\.\d{3})+,\d{1,2}\b/.test(raw) ? raw.replace(/\./g, '').replace(',', '.') : raw.replace(/,(?=\d{3}\b)/g, '').replace(/\b\d{1,3}(?:\.\d{3})+\b(?![.,]\d)/g, (m) => m.replace(/\./g, '')).replace(/(\d),(\d{1,2})\b/g, '$1.$2');
352
352
  const dc = t.match(DOLLARS_AND_CENTS);
353
353
  if (dc) {
354
354
  t = t.replace(DOLLARS_AND_CENTS, () => ` $${dc[1]}.${dc[2].padStart(2, '0')} `);
@@ -1 +1 @@
1
- {"version":3,"file":"form-state.d.ts","sourceRoot":"","sources":["../../src/helpers/form-state.js"],"names":[],"mappings":"AAQA,qDAKC;AAED,oDAEC;AAED,qFAAqF;AACrF,oEA8BC;AAuCD,2FAEC;AACD,8FAcC;AACD,2DAOC;AACD,uEA0CC;AAED,kGAAkG;AAClG,2GAOC;AACD,0HAA0H;AAC1H,uEAwBC;AACD,8HAA8H;AAC9H;;;;;EAWC;AACD,uGAAuG;AACvG;;;;;;;;;;;;QAkCC;AAED,0FAA0F;AAC1F;;;SAIC;AACD,gGAAgG;AAChG;;;;;;;;;;;EAQC;AAED,sGAAsG;AACtG,2DAIC;AACD;;;GAGG;AACH;;;;;;;SAkBC;AAED,2FAA2F;AAC3F,oGAIC;AACD,gFA4BC;AAED,2GAA2G;AAC3G;;;EASC;AACD,mEAoBC;AACD,mEAGC;AACD,sEAUC;AACD,+EAEC;AAoCD,qFAYC;AACD,oGAqCC;AACD;;;SAKC;AACD,qEAGC;AACD,gEAgBC;AACD;;uGAEuG;AACvG,sEAGC;AACD,2DAQC;AACD,oGAUC;AAED,uEAAuE;AACvE,wHA+BC;AAED,uHAAuH;AACvH,6EAaC;AACD,mHAAmH;AACnH,iGAsDC;AACD;;;GAGG;AACH;;;SAcC;AACD,+EASC;AACD,8EAeC;AA7oBM,8CAAmE"}
1
+ {"version":3,"file":"form-state.d.ts","sourceRoot":"","sources":["../../src/helpers/form-state.js"],"names":[],"mappings":"AAQA,qDAKC;AAED,oDAEC;AAED,qFAAqF;AACrF,oEA8BC;AA0CD,2FAEC;AACD,8FAcC;AACD,2DAOC;AACD,uEA6CC;AAED,kGAAkG;AAClG,2GAOC;AACD,0HAA0H;AAC1H,uEAwBC;AACD,8HAA8H;AAC9H;;;;;EAWC;AACD,uGAAuG;AACvG;;;;;;;;;;;;QAkCC;AAED,0FAA0F;AAC1F;;;SAIC;AACD,gGAAgG;AAChG;;;;;;;;;;;EAQC;AAED,sGAAsG;AACtG,2DAIC;AACD;;;GAGG;AACH;;;;;;;SAkBC;AAED,2FAA2F;AAC3F,oGAIC;AACD,gFA4BC;AAED,2GAA2G;AAC3G;;;EASC;AACD,mEAoBC;AACD,mEAGC;AACD,sEAUC;AACD,+EAEC;AAoCD,qFAYC;AACD,oGAqCC;AACD;;;SAKC;AACD,qEAGC;AACD,gEAgBC;AACD;;uGAEuG;AACvG,sEAGC;AACD,2DAQC;AACD,oGAUC;AAED,uEAAuE;AACvE,wHA+BC;AAED,uHAAuH;AACvH,6EAaC;AACD,mHAAmH;AACnH,iGAsDC;AACD;;;GAGG;AACH;;;SAcC;AACD,+EASC;AACD,8EAeC;AAnpBM,8CAAmE"}
@@ -70,10 +70,13 @@ export function resolveFormInputs(inputs, presets = {}) {
70
70
  validateFormActivation(input, resolved);
71
71
  return resolved;
72
72
  }
73
- /** Dormant entry is explicit: named descendant fields may propose local controllers, never ambient state. */
73
+ /** Dormant entry is explicit: named descendant fields may propose local controllers, never ambient state. A subform
74
+ * names its entry fields (activate_on); a plain input is its own entry ("NEW CARD, LINK, or a different amount": an
75
+ * amount said there chooses the amount option), so it declares no activate_on. */
74
76
  function validateFormActivation(input, specs) {
75
- if (!input.form || !input.activate || typeof input.activate !== 'object' || Array.isArray(input.activate) || !Object.keys(input.activate).length || !Array.isArray(input.activate_on) || !input.activate_on.length)
76
- throw new Error(`FORM activation ${input.name} needs a subform, controllers and activate_on fields`);
77
+ const plain = !input.form && !!input.profile && input.activate_on === undefined;
78
+ if (!input.activate || typeof input.activate !== 'object' || Array.isArray(input.activate) || !Object.keys(input.activate).length || !plain && (!input.form || !Array.isArray(input.activate_on) || !input.activate_on.length))
79
+ throw new Error(`FORM activation ${input.name} needs controllers, and a subform with activate_on fields or a plain input without them`);
77
80
  for (const [name, value] of Object.entries(input.activate)) {
78
81
  const controller = specs.flatMap(s => s.any_of || [s]).find(s => s.name === name);
79
82
  if (!controller?.profile || controller.form || controller.bind || name === input.name || !hasFormValue(value) || typeof value === 'object')
@@ -82,7 +85,7 @@ function validateFormActivation(input, specs) {
82
85
  if (!reading || !own(reading, 'value') || !hasFormValue(reading.value) || !formValuesEqual(reading.value, value))
83
86
  throw new Error(`FORM activation ${input.name} has invalid controller value ${name}`);
84
87
  }
85
- for (const path of input.activate_on) {
88
+ for (const path of input.activate_on || []) {
86
89
  if (typeof path !== 'string' || !path || path.split('.').some(name => !name || forbidden.has(name)))
87
90
  throw new Error(`FORM activation ${input.name} has invalid entry field`);
88
91
  if (input.schema) {
@@ -188,11 +191,14 @@ export function formInputChain(forms, id, cargo = {}) {
188
191
  for (const input of spec.any_of || [spec]) {
189
192
  if (entries && !entries.includes(input.name))
190
193
  continue;
191
- if (formCondition(input, state.values, cargo, state) !== false && !input.form && input.profile)
194
+ const inputCondition = formCondition(input, state.values, cargo, state);
195
+ // A dormant plain input that declares activate proposes its controllers when it is answered.
196
+ const inputDormant = inputCondition !== true && !activation && !input.form && input.activate !== undefined ? dormantActivation(input, state, cargo) : null;
197
+ if ((inputCondition !== false || inputDormant) && !input.form && input.profile)
192
198
  out.push({ ...input,
193
199
  ...(prefix ? { label: `${prefix}: ${input.label || input.name}`, label_es: `${prefixEs || prefix}: ${input.label_es || input.label || input.name}` } : {}),
194
200
  ...(input.choices_from && Array.isArray(state.data?.[input.choices_from]) ? { profile: { ...input.profile, choices: state.data[input.choices_from] } } : {}),
195
- ...(activation ? { activation } : {}), form: target, scope, ...(spec.any_of ? { group: spec.name, exclusive: spec.exclusive === true } : {}), value: state.values[input.name],
201
+ ...(activation || inputDormant ? { activation: activation || inputDormant } : {}), form: target, scope, ...(spec.any_of ? { group: spec.name, exclusive: spec.exclusive === true } : {}), value: state.values[input.name],
196
202
  // its declared prerequisites hold accepted values: a field that cannot be validated yet is not a context for
197
203
  // an unnamed out-of-order answer (contract, reference §5)
198
204
  ready: (input.depends_on || spec.depends_on || []).every(name => hasFormValue(state.values[name])) });
@@ -211,7 +217,7 @@ export function enrichFormMatches(matches, chain, form, names = [], asked = '')
211
217
  const enrich = h => {
212
218
  const spec = chain.find(c => c.form === h.form && c.name === h.name) || {};
213
219
  return { ...h, label: h.option_label || spec.label || h.name, label_es: h.option_label_es || spec.label_es || spec.label || h.name, ...(h.option_label ? { option: true } : {}),
214
- validator: spec.validator || '', ...(spec.response ? { response: spec.response } : {}), after: spec.after || '', validate_always: spec.validate_always === true, ...(spec.activation ? { activation: spec.activation } : {}), pii: spec.pii === true, current: h.form === form && names.includes(h.name), asked };
220
+ validator: spec.validator || '', ...(spec.response ? { response: spec.response } : {}), after: spec.after || '', validate_always: spec.validate_always === true, ...(spec.activation ? { activation: spec.activation } : {}), ...(spec.silent === true ? { silent: true } : {}), pii: spec.pii === true, current: h.form === form && names.includes(h.name), asked };
215
221
  };
216
222
  return { ...matches, hits: matches.hits.map(enrich), ambiguous: matches.ambiguous.map(enrich), remaining: (matches.remaining || []).map(group => group.map(enrich)) };
217
223
  }
@@ -1 +1 @@
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;;;;;;;;;;;;;;;;;;;;;;EAoLC"}
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;;;;;;;;;;;;;;;;;;;;;;EAoMC"}
@@ -299,6 +299,9 @@ export function matchInputs(answer, candidates, current, choiceKey) {
299
299
  const spanText = (reading) => (reading.spans || []).map(([a, b]) => raw.slice(a, b)).join(' ');
300
300
  const refused = [];
301
301
  let readings = [];
302
+ // An invited input is read as in order while it is asked (docs/copilot-reference.md §5): its asked signature, not
303
+ // its narrower out-of-order one ("40" at "NEW CARD, LINK, or a different amount" is the amount).
304
+ const invitedInput = new Set([...list.filter(isCur).flatMap((c) => Array.isArray(c.invites) ? c.invites : []), ...(Array.isArray(cur.invites) ? cur.invites : [])]);
302
305
  for (const c of list) {
303
306
  // Rule 2: a confirmation is answered only when asked, firm by CONFIRM_WORDS, soft by the wider YES vocabulary.
304
307
  const confirmation = ['firm', 'soft'].includes(c.confirmation) ? c.confirmation : '';
@@ -313,7 +316,7 @@ export function matchInputs(answer, candidates, current, choiceKey) {
313
316
  }
314
317
  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;
315
318
  const profile = narrativeCurrent && vocabulary.kind === 'email' ? { ...vocabulary, literal: true } : vocabulary;
316
- let r = readInputProfile(profile, raw, isCur(c));
319
+ let r = readInputProfile(profile, raw, isCur(c) || invitedInput.has(c.name) && c.confirmation === undefined);
317
320
  // A semantic choice supplied by ask-choice (e.g. spoken keypad keys) is a reading, not an early return.
318
321
  if (!r && c === own && choiceKey && c.profile.kind === 'choice' && (c.profile.choices || []).some(x => x.key === choiceKey)) {
319
322
  r = { ...(r || {}), value: choiceKey, spans: r?.spans?.length ? r.spans : [[0, raw.length]] };
@@ -346,6 +349,20 @@ export function matchInputs(answer, candidates, current, choiceKey) {
346
349
  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));
347
350
  // Rule 4: a bare number has no money mark and no word in its evidence.
348
351
  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)));
352
+ // Rule 1 by shape: a bare number whose length is the current question's own keypad length (`digits`) answers that
353
+ // question even when its reader rejects it (an expiration "1320" is month 13); its validation decides and the
354
+ // question is asked again. It is never read out of order as another field (that "1320" is not a security code).
355
+ // A choice is excluded (a digit it does not offer reaches the invited amount, rule 4), and so is an amount.
356
+ const bareDigits = raw.replace(/\D/g, '');
357
+ const shapeOf = own && own.digits && !['choice', 'amount', 'text'].includes(own.profile.kind) ? own.digits : null;
358
+ if (shapeOf && bareDigits && /^[\d\s./-]+$/.test(raw.trim()) && !isAmountAnswer(raw) && bareDigits.length >= Number(shapeOf.min || 1) && bareDigits.length <= Number(shapeOf.max || shapeOf.min || 1)
359
+ && !readings.some((reading) => reading.current || invited(reading))) {
360
+ readings.push({ form: own.form, name: own.name, value: bareDigits, ...(own.validator ? {} : { invalid: true }), known: false, repeatedChoice: false, wholeChoice: false, choice: false, narrative: false, resetOn: [], spans: [[0, raw.length]], keypad: false, fallback: false,
361
+ group: own.group, exclusive: own.exclusive === true, level: forms.indexOf(own.scope || own.form), current: true, confirm: Array.isArray(own.confirm_against) ? own.confirm_against : [] });
362
+ }
363
+ else {
364
+ // not a bare number of the current question's shape, or the current question read it
365
+ }
349
366
  readings = readings.filter((reading) => {
350
367
  if (inOrder(reading)) {
351
368
  if (!reading.current)
@@ -1 +1 @@
1
- export const JSFE_RUNTIME_FINGERPRINT = "sha256:25d1acd637baced87978aa81eb3c47112eb8d82d1021dfb3fedc68aba0b8c988";
1
+ export const JSFE_RUNTIME_FINGERPRINT = "sha256:d1d531f3fad961b1d17d88b5fb63fffff2d5ddfaf837882fe0345bc496ce5dc2";
@@ -149,7 +149,7 @@ Use one of these input shapes:
149
149
 
150
150
  | Shape | Fields |
151
151
  | --- | --- |
152
- | Preset field | `name`, `field`; optional overrides. Presets: `phone`, `email`, `zip`, `account_number`, `ssn_last4`, `amount`, `street_address`, `city`, `state`, `card_number`, `card_expiration`, `card_cvv`, `card_zip`. |
152
+ | Preset field | `name`, `field`; optional overrides. Presets: `phone`, `email`, `zip`, `account_number`, `ssn_last4`, `amount`, `street_address`, `city`, `state`, `card_number`, `card_expiration`, `card_cvv`, `card_zip`. `card_cvv` takes its card brand's length (`validate-card-cvv`: 4 digits for American Express, 3 for every other brand). |
153
153
  | Inline field | `name`, `profile`, questions and business validation. |
154
154
  | Child form | `name`, `form:"installed-child-id"`, optional `parameters`. JSFE links its schema; do not copy the derived `schema` into editable source. |
155
155
  | Alternative group | `name`, `any_of:[leaf-or-child declarations]`; optional `exclusive:true`. At least one applicable member satisfies the group; exclusive competing answers require clarification. |
@@ -172,7 +172,8 @@ Field options:
172
172
  | `choices_from`, `question_from` | Read prepared choices/question from the owner's `data`; corresponding localized question storage is handled by shared workers. |
173
173
  | `digits`, `pii`, `as_spoken` | Collection hints. FORM preserves original speech by default; false explicitly opts into cleanup. |
174
174
  | `confirm_against` | Declare overlapping field interpretations that require clarification even when normal current-field priority applies. |
175
- | `activate`, `activate_on` | Narrow dormant-child activation: proposed controller assignments plus nominated descendant entry paths. Controller and entry validate atomically; a rejected entry must not flip a controller. |
175
+ | `activate`, `activate_on` | Narrow dormant activation: proposed controller assignments plus, for a subform, nominated descendant entry paths (`activate_on`). A dormant plain input declares `activate` alone and is its own entry: an amount said at "NEW CARD, LINK, or a different amount" chooses the amount option (`revised_amount` with `when:{next:'amount'}`, `activate:{next:'amount'}`). Controller and entry validate atomically; a rejected entry must not flip a controller. |
176
+ | `silent` | An accepted answer is not acknowledged. For an input that hands its value on to another form's input (its `after` flow), whose own acceptance or rejection is what the caller hears. |
176
177
  | `response` | `{description}` on a readable leaf; use existing contextual DISPATCH extraction before accepting an open-ended answer. |
177
178
  | `proposal` | Optional approval of an inferred field candidate, independent of final action consent. |
178
179
  | `max_misses`, `max_attempts`, `on_exhausted` | Bounds for unreadable responses, validator rejections and deliberate branch recovery. |
@@ -214,7 +215,10 @@ Declarations:
214
215
  Rules, in order:
215
216
 
216
217
  1. **The current question first.** The whole answer is read against the current input's signature and the inputs it
217
- invites. A match is that answer.
218
+ invites (an invited input with its asked signature, not its out-of-order one). A match is that answer. A bare number
219
+ whose length is the current input's keypad length (`digits`) is its answer even when its reader rejects it: its
220
+ validation decides and the question is asked again, so an expiration "1320" (month 13) is never a security code.
221
+ A choice keeps rule 4, and an amount is read by its own profile.
218
222
  2. **A confirmation is answered only when it is asked.** Out of order it never makes sense, so no words, YES, NO or the
219
223
  option's own phrases, fill a confirmation that is not the current question.
220
224
  3. **Out of order requires a ready field and the whole signature.** A bare number never fills, out of order, a field whose
@@ -242,3 +242,13 @@ counted as an attempt, so `max_attempts` still bounds it; `abort` ends every flo
242
242
  detection. A host fixture that answered such turns with intent detection only must now also answer `rank_reply`.
243
243
 
244
244
  **A press verb before a key picks it:** "ya oprimí el uno", "I already pressed one", "marqué el 2" (not after a negation).
245
+
246
+ ## 1.12.0 — a confirmation's unread answer goes to the reply ranker
247
+
248
+ ConfirmForm no longer hands a long unread answer (three or more words, or any words when the form holds an amount) to the
249
+ host with a plain DISPATCH. The new shared worker `read-confirm-reply` ranks it against the confirmation question:
250
+ `answers` with the reply declining (stage-2 `key` = `no`) is the confirmation's NO, so the form's decline path runs;
251
+ `answers` with nothing declined asks the confirmation again, counted as a miss (two misses still hand off, as before);
252
+ `abort` ends every flow; `new_request` goes to intent detection. The ranker is offered NO only: a confirmation is accepted
253
+ only by its exact YES, never by a ranked reading. Hosts that ship the shared workers in a system set must add
254
+ `read-confirm-reply`; a host fixture that answered such turns with intent detection only must now also answer `rank_reply`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jsfe",
3
- "version": "1.11.1",
3
+ "version": "1.13.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",