jsfe 1.29.0 → 1.31.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.
- package/README.md +2 -0
- package/dist/docs/flow-docs.d.ts.map +1 -1
- package/dist/docs/flow-docs.js +15 -1
- package/dist/forms/flows.d.ts.map +1 -1
- package/dist/forms/flows.js +14 -3
- package/dist/forms/identity-flows.js +2 -2
- package/dist/helpers/form-state.d.ts.map +1 -1
- package/dist/helpers/form-state.js +7 -1
- package/dist/helpers/index.d.ts +5 -1
- package/dist/helpers/index.d.ts.map +1 -1
- package/dist/helpers/index.js +3 -3
- package/dist/helpers/match-inputs.d.ts +5 -0
- package/dist/helpers/match-inputs.d.ts.map +1 -1
- package/dist/helpers/match-inputs.js +49 -4
- package/dist/runtime-identity.js +1 -1
- package/docs/architecture-guide.md +2 -0
- package/docs/copilot-reference.md +9 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -379,6 +379,8 @@ instructions with its text model.
|
|
|
379
379
|
|
|
380
380
|
An input with `profile.whole_phrases` (default off; `choice`, or a `pattern` that reads words) keeps its word reader only for a reply wholly made of its phrase, arguments, courtesy, connecting words and its `template_words`; any other reply goes to this ranker.
|
|
381
381
|
|
|
382
|
+
**A number that must name itself.** `input.confirm_out_of_order: {word, word_es, message, message_es}` (default off): out of order a number fills this input only beside its word, in either language ("account 5373850"). Alone it may be something else, a code said with an extra digit, so nothing is filled: the caller hears `message` (`{value}` the number, digit by digit on a call; `{word}` the word) and the current question again, which counts as an attempt at that question. It works at `collect-input` and `confirm-form` questions. At its own question the number needs no word. `authenticate-user`'s `code_confirm` takes the same declaration for a `code_validator`, whose flow reads it with the helpers `saysConfirmWord(answer, spec)` and `confirmOutOfOrderLine(spec, value, language, voice)`.
|
|
383
|
+
|
|
382
384
|
**Declared choices (`responseChoices`, jsfe 1.14):** a step may name a flow variable of type array holding
|
|
383
385
|
`[{ key, label, parameters? }]` (`parameters`: extracted on a pick, read as `{ key, arguments }`); the ranker is then also offered `choice:<key>` for each (before the fixed options), and `answers`
|
|
384
386
|
means a reply that picks none of them. A picked choice sets `responseVariable` to `{ key }`, plus `arguments` when it declares `parameters` (the pick stands if extraction finds nothing or fails);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"flow-docs.d.ts","sourceRoot":"","sources":["../../src/docs/flow-docs.js"],"names":[],"mappings":"AAkBA;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"flow-docs.d.ts","sourceRoot":"","sources":["../../src/docs/flow-docs.js"],"names":[],"mappings":"AAkBA;;;;;;;;;;;EA+RC"}
|
package/dist/docs/flow-docs.js
CHANGED
|
@@ -410,7 +410,7 @@ export function generateFlowDocs({ sets, root, check = false, hostRun = {}, log
|
|
|
410
410
|
}
|
|
411
411
|
else { /* none */ }
|
|
412
412
|
if (flow.provides) {
|
|
413
|
-
out.push('', '### Provider', '', ...(flow.provides.facts || []).map((f) => `- fact \`${f.key}\`: ${f.description || ''}${f.path ? ' (from `' + f.path + '`)' : ''}`), '', providerChart(flow.provides));
|
|
413
|
+
out.push('', '### Provider', '', ...(flow.provides.facts || []).map((f) => `- fact \`${f.key}\`: ${f.description || ''}${f.path ? ' (from `' + f.path + '`)' : ''}`), ...providerMenuNotes(flow.provides.menu), '', providerChart(flow.provides));
|
|
414
414
|
}
|
|
415
415
|
else { /* not a provider */ }
|
|
416
416
|
if (flow.form) {
|
|
@@ -441,6 +441,20 @@ export function generateFlowDocs({ sets, root, check = false, hostRun = {}, log
|
|
|
441
441
|
else { /* no steps */ }
|
|
442
442
|
return out.join('\n');
|
|
443
443
|
}
|
|
444
|
+
// What a provider's menu opted into: both change what the caller hears, and neither shows in the chart.
|
|
445
|
+
function providerMenuNotes(menu) {
|
|
446
|
+
const notes = [];
|
|
447
|
+
if (menu && menu.whole_phrases) {
|
|
448
|
+
notes.push('- menu `whole_phrases`: a fact\'s phrase inside a sentence goes to the reply ranker; a fact pick also answers every fact that cleared 0.6');
|
|
449
|
+
}
|
|
450
|
+
else { /* not opted in */ }
|
|
451
|
+
const u = menu && menu.unanswered;
|
|
452
|
+
if (u) {
|
|
453
|
+
notes.push(`- menu \`unanswered\`: a question with no fact, back from this menu${u.asked ? ' (asked: ' + u.asked.join(', ') + ')' : ''}, says "${u.question}"${u.question_es ? ' / "' + u.question_es + '"' : ''}; yes runs menu choice \`${u.choice}\`, no returns to the menu`);
|
|
454
|
+
}
|
|
455
|
+
else { /* not opted in */ }
|
|
456
|
+
return notes;
|
|
457
|
+
}
|
|
444
458
|
function upsert(file, title, generated, skeleton) {
|
|
445
459
|
const block = `${START}\n${generated}\n${END}`;
|
|
446
460
|
const old = fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : null;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"flows.d.ts","sourceRoot":"","sources":["../../src/forms/flows.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"flows.d.ts","sourceRoot":"","sources":["../../src/forms/flows.js"],"names":[],"mappings":"AAsIA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA8DC;AACD;;;GAGG;AACH;;;;;;EAGC;AAshBD,wHAAwH;AACxH,wEAaC;AACD,4FAA4F;AAC5F;;;EAYC;AACD,sGAAsG;AACtG;;;EAOC;AA7tBM,6CAAuG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAs7B9G;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAOG"}
|
package/dist/forms/flows.js
CHANGED
|
@@ -84,7 +84,8 @@ export const FIELD_PRESETS = {
|
|
|
84
84
|
card_expiration: { label: 'expiration date', label_es: 'fecha de vencimiento',
|
|
85
85
|
// MM0YY: a zero said between month and year ("020 28", prod 2026-10-25 14:10, was taken for the billing ZIP).
|
|
86
86
|
// Up to two words between month and year ("el mes 06 año 31", prod 2026-10-28 21:54, was asked again); a month by name.
|
|
87
|
-
|
|
87
|
+
// A spoken separator with the digits apart: "0 4 guion 3 1" (owner 2026-11-02; prod 2026-11-01 15:19 was asked again).
|
|
88
|
+
profile: { kind: 'pattern', regex: `\\b(0?[1-9]|1[0-2]|${MONTH_RE})\\.?(?:\\s*(?:/|-|del?|año|ano|year|of)(?=[\\s\\d])){0,2}\\s*(?:20)?(\\d{2})\\b|\\b0[1-9]0\\s+(?:20)?\\d{2}\\b|\\b(?:0\\s?[1-9]|1\\s?[0-2]|[1-9])\\s*(?:gui[oó]n|dash|hyphen|slash|barra|diagonal)\\s*(?:2\\s?0\\s?)?\\d\\s?\\d\\b` }, digits: { min: 3, max: 6 },
|
|
88
89
|
validator: 'validate-card-expiration',
|
|
89
90
|
question: 'What is the expiration date, month and year?', question_es: '¿Cuál es la fecha de vencimiento, mes y año?',
|
|
90
91
|
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.',
|
|
@@ -254,7 +255,7 @@ const collectInput = {
|
|
|
254
255
|
id: 'collect-input', name: 'CollectInput', version: '1.0.0', scope: 'isolated',
|
|
255
256
|
description: "Worker: ask the form's current input once and file the answer wherever it belongs in the form chain (matchInputs, decided by scope; a declared confirm_against pair is asked). One reading → collect-input-accept; ambiguous → disambiguate-input; a question about the form's data → provide-data; a short miss → counted, the loop re-asks; 3+ words that fit nothing → DISPATCH. Params: form, current, questions, provides, exit_context(_es).",
|
|
256
257
|
parameters: ['form', 'current', 'questions', 'provides', 'exit_context', 'exit_context_es'].map((n) => ({ name: n, type: ['questions'].includes(n) ? 'array' : n === 'provides' ? 'object' : 'string', description: n })),
|
|
257
|
-
variables: vars({ ci_partial: '', ci_no_owner: '', ci_no_flow: '', ci_no_up: false, ci_reply_reader: '', ci_exhaustion: null, ci_d: {}, ci_pre: '', ci_val: '', ci_routed: '', ci_names: [], ci_chain: [], ci_spec: null, ci_choices: [], ci_q: '', ci_q_es: '', ci_missed: 0, ci_max: 3, ci_misses: 0, ci_miss_limit: 2, ci_must: false, ci_m: null, ci_qkey: '', ci_skip_gate: '', ci_words: 0, ci_digits: null, ci_amount: false, ci_as_spoken: false, ci_reader: {}, ci_pii: false, ci_ctx: '', ci_ctx_es: '' }),
|
|
258
|
+
variables: vars({ ci_clarify: null, ci_partial: '', ci_no_owner: '', ci_no_flow: '', ci_no_up: false, ci_reply_reader: '', ci_exhaustion: null, ci_d: {}, ci_pre: '', ci_val: '', ci_routed: '', ci_names: [], ci_chain: [], ci_spec: null, ci_choices: [], ci_q: '', ci_q_es: '', ci_missed: 0, ci_max: 3, ci_misses: 0, ci_miss_limit: 2, ci_must: false, ci_m: null, ci_qkey: '', ci_skip_gate: '', ci_words: 0, ci_digits: null, ci_amount: false, ci_as_spoken: false, ci_reader: {}, ci_pii: false, ci_ctx: '', ci_ctx_es: '' }),
|
|
258
259
|
steps: [
|
|
259
260
|
SET('ci_find_spec', 'ci_spec', "(function () { var s = ((cargo.forms[form] || {}).specs || []).filter(function (s) { return s.name === current; })[0]; return s ? (Array.isArray(s.any_of) ? Object.assign({}, s, { profile: { kind: 'group' } }) : s) : null; })()"),
|
|
260
261
|
SET('ci_current_names', 'ci_names', "Array.isArray(ci_spec && ci_spec.any_of) ? ci_spec.any_of.map(function (m) { return m.name; }) : [current]"),
|
|
@@ -327,6 +328,11 @@ const collectInput = {
|
|
|
327
328
|
WHEN('ci_explicit_question', 'ci_m.questions.length > 0', FLOW('ci_explicit_provide', 'provide-data', 'call', { form: '{{form}}', provides: '{{provides}}', asked: '{{ci_qkey}}', asked_keys: '{{ci_m.questions}}', no_menu: 'true', no_gate: '{{ci_skip_gate}}' })),
|
|
328
329
|
WHEN('ci_explicit_question_done', 'ci_m.questions.length > 0', END('ci_explicit_question_end')),
|
|
329
330
|
SET('ci_reply_reader', 'ci_reply_reader', "ci_choices.length ? 'read-form-choice' : 'read-form-reply'"),
|
|
331
|
+
// input.confirm_out_of_order: the number filled nothing; say how to mean it, then the question again. It counts as an attempt.
|
|
332
|
+
SET('ci_clarify', 'ci_clarify', "!ci_m.hits.length && !ci_m.ambiguous.length && !ci_m.questions.length && Array.isArray(ci_m.clarify) && ci_m.clarify.length ? ci_m.clarify[0] : null"),
|
|
333
|
+
WHEN('ci_clarify_attempt', 'ci_clarify', SIDE('ci_clarify_count', "(cargo.forms[form].attempts=cargo.forms[form].attempts || {},cargo.forms[form].attempts[current]=Number(cargo.forms[form].attempts[current] || 0)+1,true)")),
|
|
334
|
+
WHEN('ci_clarify_say', 'ci_clarify', SAY('ci_clarify_line', "{{confirmOutOfOrderLine(ci_clarify,ci_clarify.value,'en',cargo.voice)}}", "{{confirmOutOfOrderLine(ci_clarify,ci_clarify.value,'es',cargo.voice)}}")),
|
|
335
|
+
WHEN('ci_clarify_end', 'ci_clarify', END('ci_clarify_done')),
|
|
330
336
|
{ id: 'ci_route', type: 'CASE', branches: {
|
|
331
337
|
'condition: ci_m.ambiguous.length > 0': FLOW('ci_ambiguous', 'disambiguate-input', 'call', { readings: '{{ci_m.ambiguous}}', hits: '{{ci_m.hits}}', remaining: '{{ci_m.remaining}}' }),
|
|
332
338
|
'condition: ci_m.hits.length > 0': FLOW('ci_accept', 'collect-input-accept', 'call', { hits: '{{ci_m.hits}}' }),
|
|
@@ -595,7 +601,7 @@ const confirmForm = {
|
|
|
595
601
|
id: 'confirm-form', name: 'ConfirmForm', version: '1.0.0', scope: 'isolated',
|
|
596
602
|
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 answer with words it cannot read goes to the reply ranker (read-confirm-reply); an unclear one is asked again, and after two the give-up worker takes over. Result: cargo.formConfirm[form] = 'yes' | 'no' | 'changed'. Params: form, question(_es), exit_context(_es).",
|
|
597
603
|
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" }]),
|
|
598
|
-
variables: vars({ named_fields: false, reply_context: '', cf_must: false, 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: '' }),
|
|
604
|
+
variables: vars({ cf_clarify: null, named_fields: false, reply_context: '', cf_must: false, 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: '' }),
|
|
599
605
|
steps: [
|
|
600
606
|
SIDE('cf_reset', "(cargo.formConfirm = cargo.formConfirm || {}, cargo.formConfirm[form] = '', cargo.confirmRanked = '', true)"),
|
|
601
607
|
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};}))`),
|
|
@@ -621,7 +627,11 @@ const confirmForm = {
|
|
|
621
627
|
WHEN('cf_question_fields', 'cf_m.questions.length > 0 && cf_m.hits.length > 0', FLOW('cf_question_accept', 'collect-input-accept', 'call', { hits: '{{cf_m.hits}}' })),
|
|
622
628
|
WHEN('cf_question_provide', 'cf_m.questions.length > 0', FLOW('cf_question_answer', 'provide-data', 'call', { form: '{{form}}', provides: '{{cf_provides}}', asked_keys: '{{cf_m.questions}}', no_menu: 'true', no_gate: '{{cf_skip_gate}}' })),
|
|
623
629
|
SET('cf_must_complete', 'cf_must', "formMustComplete(cargo,form,'__confirm')"),
|
|
630
|
+
// input.confirm_out_of_order: a number that filled nothing; say how to mean it, then the question again. A try, without "sorry".
|
|
631
|
+
SET('cf_clarify', 'cf_clarify', "cargo.choice !== 'yes' && !cf_m.questions.length && !cf_m.ambiguous.length && !cf_m.hits.length && Array.isArray(cf_m.clarify) && cf_m.clarify.length ? cf_m.clarify[0] : null"),
|
|
632
|
+
WHEN('cf_clarify_say', 'cf_clarify', SAY('cf_clarify_line', "{{confirmOutOfOrderLine(cf_clarify,cf_clarify.value,'en',cargo.voice)}}", "{{confirmOutOfOrderLine(cf_clarify,cf_clarify.value,'es',cargo.voice)}}")),
|
|
624
633
|
{ id: 'cf_route', type: 'CASE', branches: {
|
|
634
|
+
'condition: cf_clarify': SET('cf_clarified', 'cf_result', "'unclear'"),
|
|
625
635
|
'condition: cf_m.questions.length > 0': SET('cf_question_repeat', 'cf_result', "'changed'"),
|
|
626
636
|
"condition: cargo.choice === 'yes'": SET('cf_answered', 'cf_result', "'yes'"),
|
|
627
637
|
'condition: cf_m.ambiguous.length > 0': FLOW('cf_ambiguous', 'disambiguate-input', 'call', { readings: '{{cf_m.ambiguous}}', hits: '{{cf_m.hits}}', remaining: '{{cf_m.remaining}}' }),
|
|
@@ -640,6 +650,7 @@ const confirmForm = {
|
|
|
640
650
|
optionalIncomplete('cf_optional_exhausted', 'cf_tries >= 2 && !cf_must', 'form', "'confirmation_misses_exhausted'"),
|
|
641
651
|
WHEN('cf_misses_stamp', 'cf_tries >= 2 && !cf_must', recoveryStamp('cf_failure_stamp', 'form', "'confirmation_misses_exhausted'")),
|
|
642
652
|
WHEN('cf_misses_exhausted', 'cf_tries >= 2 && !cf_must', FLOW('cf_give_up', 'give-up', 'reboot', { context: '{{exit_context}}', context_es: '{{exit_context_es}}' })),
|
|
653
|
+
WHEN('cf_clarified_no_sorry', 'cf_clarify', SIDE('cf_clarified_feedback', "consumeFormFeedback(cargo,form,'__confirm')")),
|
|
643
654
|
WHEN('cf_bind_revision', "cf_result === 'yes'", SIDE('cf_bind', 'confirmFormRevision(cargo, form)')),
|
|
644
655
|
SIDE('cf_store', "(cargo.formConfirm[form] = cf_result === 'unclear' || cf_result === 'clarification' ? 'changed' : cf_result, true)"),
|
|
645
656
|
],
|
|
@@ -150,8 +150,8 @@ const contact = worker('get-cell-or-email', 'GetCellOrEmail', 'Identify a suppli
|
|
|
150
150
|
const declined = worker('contact-caller-declined', 'ContactCallerDeclined', 'Repeat contact collection without offering caller ID.', { parameters: [param('contact_options', 'Current contact service options', 'object')], steps: [set('contact_declined_options', 'contact_options', "Object.assign({},contact_options,{caller_on_file:'no',intro:'',intro_es:''})"), contactReplay('contact_declined_repeat')] });
|
|
151
151
|
const splitFailed = worker('contact-split-failed', 'ContactSplitFailed', 'Restart a split identifier after too many digits.', { parameters: [param('contact_options', 'Current contact service options', 'object')], steps: [set('contact_split_options', 'contact_options', "Object.assign({},contact_options,{caller_on_file:'no',intro:cargo.voice?'I could not put that number together. Please enter it on your keypad.':'I could not put that number together. Please type the whole number.',intro_es:cargo.voice?'No pude juntar ese número. Por favor ingréselo con el teclado.':'No pude juntar ese número. Por favor escriba el número completo.'})"), contactReplay('contact_split_repeat')] });
|
|
152
152
|
const rejected = worker('contact-not-on-file', 'ContactNotOnFile', 'Re-collect a rejected account contact, then offer the common retry menu at the rejection limit.', { parameters: [param('contact_options', 'Current contact service options', 'object'), param('error_message', 'Rejection sentence'), param('error_message_es', 'Spanish rejection sentence')], steps: [set('contact_rejected_options', 'contact_options', "Object.assign({},contact_options,{caller_on_file:cargo.contactKind === 'caller' || contact_options.caller_on_file === 'no' ? 'no':'',fromMenu:false,intro:error_message,intro_es:error_message_es})"), when('contact_rejected_again', 'cargo.gceRejects <= 2', contactReplay('contact_rejected_collect', { gce_rejected: true })), set('contact_rejected_error', 'contact_error', 'error_message'), set('contact_rejected_error_es', 'contact_error_es', 'error_message_es'), contactMenu('contact_rejected_menu')] });
|
|
153
|
-
const AUTH_DEFAULTS = { retry_flow: '', cancel_flow: '', cell_number: '', email: '', email_validator: '', cell_validator: '', code_validator: '', answer_handler: '', retry_returns: false, retrying: false, declined_caller: false, parent_form: '', task: '', task_es: '', failed_flow: '', contact_valid: false, auth_result: false, auth_contact_error: '', auth_contact_error_es: '', auth_previous_options: null, auth_restart: false, auth_join: false, otp_expiry_ms: OTP_EXPIRY_MS };
|
|
154
|
-
const authKeys = ['retry_flow', 'cancel_flow', 'email_validator', 'cell_validator', 'code_validator', 'answer_handler', 'retry_returns', 'parent_form', 'task', 'task_es', 'failed_flow'];
|
|
153
|
+
const AUTH_DEFAULTS = { retry_flow: '', cancel_flow: '', cell_number: '', email: '', email_validator: '', cell_validator: '', code_validator: '', code_confirm: '', answer_handler: '', retry_returns: false, retrying: false, declined_caller: false, parent_form: '', task: '', task_es: '', failed_flow: '', contact_valid: false, auth_result: false, auth_contact_error: '', auth_contact_error_es: '', auth_previous_options: null, auth_restart: false, auth_join: false, otp_expiry_ms: OTP_EXPIRY_MS };
|
|
154
|
+
const authKeys = ['retry_flow', 'cancel_flow', 'email_validator', 'cell_validator', 'code_validator', 'code_confirm', 'answer_handler', 'retry_returns', 'parent_form', 'task', 'task_es', 'failed_flow'];
|
|
155
155
|
// One destination's sends, by phone (last 10 digits) or email: a caller gets 3 codes per contact (owner 2026-11-01).
|
|
156
156
|
export const OTP_DESTINATION_JS = "(cargo.otp_cell_number ? String(cargo.otp_cell_number).replace(/[^0-9]/g,'').slice(-10) : String(cargo.otp_email || '').toLowerCase())";
|
|
157
157
|
export const MAX_OTP_SENDS_PER_CONTACT = 3;
|
|
@@ -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,
|
|
1
|
+
{"version":3,"file":"form-state.d.ts","sourceRoot":"","sources":["../../src/helpers/form-state.js"],"names":[],"mappings":"AAQA,qDAKC;AAED,oDAEC;AAED,qFAAqF;AACrF,oEA+BC;AA0CD,2FAEC;AACD,8FAcC;AACD,2DAOC;AACD,uEA8CC;AAED,kGAAkG;AAClG,2GAOC;AACD,0HAA0H;AAC1H,uEAwBC;AACD,8HAA8H;AAC9H;;;;;EAeC;AACD,uGAAuG;AACvG;;;;;;;;;;;;;;QAuCC;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;AA9pBM,8CAAmE"}
|
|
@@ -49,6 +49,8 @@ export function resolveFormInputs(inputs, presets = {}) {
|
|
|
49
49
|
throw new Error('FORM proposal requires a readable leaf input; declare it on each group member or inside the subform');
|
|
50
50
|
validateFormProposalPolicy(input.proposal);
|
|
51
51
|
}
|
|
52
|
+
if (input.confirm_out_of_order !== undefined && (typeof input.confirm_out_of_order?.word !== 'string' || !input.confirm_out_of_order.word || typeof input.confirm_out_of_order.message !== 'string'))
|
|
53
|
+
throw new Error('FORM confirm_out_of_order needs a word and a message');
|
|
52
54
|
if (input.must_complete !== undefined && typeof input.must_complete !== 'boolean')
|
|
53
55
|
throw new Error('FORM must_complete must be true or false');
|
|
54
56
|
if (input.max_misses !== undefined && (!Number.isInteger(input.max_misses) || input.max_misses < 1))
|
|
@@ -219,7 +221,7 @@ export function enrichFormMatches(matches, chain, form, names = [], asked = '')
|
|
|
219
221
|
const enrich = h => {
|
|
220
222
|
const spec = chain.find(c => c.form === h.form && c.name === h.name) || {};
|
|
221
223
|
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 } : {}),
|
|
222
|
-
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 };
|
|
224
|
+
validator: spec.validator || '', ...(!(h.form === form && names.includes(h.name)) && spec.profile && !['choice', 'amount', 'text'].includes(spec.profile.kind) && /\d/.test(String(h.value)) ? { out_of_order_number: true } : {}), ...(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 };
|
|
223
225
|
};
|
|
224
226
|
return { ...matches, hits: matches.hits.map(enrich), ambiguous: matches.ambiguous.map(enrich), remaining: (matches.remaining || []).map(group => group.map(enrich)) };
|
|
225
227
|
}
|
|
@@ -276,6 +278,10 @@ export function expandFormActivations(cargo, hits = []) {
|
|
|
276
278
|
}
|
|
277
279
|
/** No partial activation: validation failure suppresses inferred changes; pending prerequisites retain the complete group. */
|
|
278
280
|
export function settleFormActivations(batch = {}, cargo = null) {
|
|
281
|
+
// A number read out of order is taken only when it is valid (owner 2026-11-02): one its validator rejects fills nothing and
|
|
282
|
+
// says nothing, and the current question is asked again.
|
|
283
|
+
const invalidNumbers = (batch.rejected || []).filter(h => h.out_of_order_number === true);
|
|
284
|
+
batch = { ...batch, rejected: (batch.rejected || []).filter(h => h.out_of_order_number !== true), ignored: [...(batch.ignored || []), ...invalidNumbers] };
|
|
279
285
|
const accepted = [...(batch.accepted || [])], rejected = [...(batch.rejected || [])], pending = [...(batch.pending || [])];
|
|
280
286
|
const all = accepted.concat(rejected, pending), keys = new Set(all.map(h => h.activation_group).filter(Boolean));
|
|
281
287
|
const held = new Set(), discarded = new Set();
|
package/dist/helpers/index.d.ts
CHANGED
|
@@ -32,6 +32,8 @@ export namespace STANDARD_FUNCTIONS {
|
|
|
32
32
|
export { isPartialMoneyAnswer };
|
|
33
33
|
export { validateMoneyOptions };
|
|
34
34
|
export { takeChoiceOffer };
|
|
35
|
+
export { saysConfirmWord };
|
|
36
|
+
export { confirmOutOfOrderLine };
|
|
35
37
|
export { matchesChoice };
|
|
36
38
|
export { isAnswer };
|
|
37
39
|
export { isAgentRequest };
|
|
@@ -154,6 +156,8 @@ import { isPastPaymentStatement } from './money.js';
|
|
|
154
156
|
import { isPartialMoneyAnswer } from './money.js';
|
|
155
157
|
import { validateMoneyOptions } from './money.js';
|
|
156
158
|
import { takeChoiceOffer } from './choice-offer.js';
|
|
159
|
+
import { saysConfirmWord } from './match-inputs.js';
|
|
160
|
+
import { confirmOutOfOrderLine } from './match-inputs.js';
|
|
157
161
|
import { matchesChoice } from './matches-choice.js';
|
|
158
162
|
import { ANSWER_WORDS } from './answer.js';
|
|
159
163
|
import { CONFIRM_WORDS } from './answer.js';
|
|
@@ -249,5 +253,5 @@ import { validatePhone } from './validators.js';
|
|
|
249
253
|
import { validateEmail } from './validators.js';
|
|
250
254
|
import { urlToSpeech } from './url-speech.js';
|
|
251
255
|
import { textWithUrlToSpeech } from './url-speech.js';
|
|
252
|
-
export { continuationSafetyProtocol, cancelFormTask, contextualResponseProtocol, validateFormResponse, hasFormResponse, formProposalPolicy, stageFormProposal, nextFormProposal, formProposalReadings, bindFormProposal, decideFormProposal, commitFormProposals, settleFormProposalValidation, formProposalProtocol, validateFormProposalPolicy, validateFormProposalDescriptor, formProposalDescriptor, clearFormMisses, formNoOwner, missFormInput, formMustComplete, formInputPrompt, formAcknowledges, consumeFormFeedback, formConversationOutcome, readMoneyAmount, isMoneyAmount, moneyIntentAllows, isPastPaymentStatement, isPartialMoneyAnswer, validateMoneyOptions, takeChoiceOffer, matchesChoice, ANSWER_WORDS, CONFIRM_WORDS, isAnswer, isAgentRequest, isExitRequest, isOtherRequest, isSpelledFragment, isAmountAnswer, amountInAnswer, pickChoice, withoutPromptEcho, readEmail, readPhone, readInternationalPhone, readIdentifier, last4, maskEmail, callerTextNumber, textDestination, callerIsInternational, normalizeAndFindCapture, anythingElse, goodbye, sayName, sorryNotCaught, dateToWords, amountToSpeech, validateCardNumber, validateExpiration, consumeActionReceipt, authorizeFormActionRetry, formSignature, selectFormAction, confirmFormRevision, prepareFormAction, actionConfirmationValid, beginFormAction, finishFormAction, actionGateVerified, sourceNeedsLoad, beginSourceRefresh, completeSourceRefresh, pickProviderQuestion, matchInputs, readFormAnswer, readInputProfile, formAcceptanceTrace, resolveFormInputs, formInputApplies, formInputFilled, nextFormInput, formInputChain, openFormInstance, validateFormIncompleteOutcome, formIncompleteOutcome, validateFormExhaustion, exhaustFormInput, formInitialReadings, orderFormReadings, proposedFormValues, applyFormValidation, formReadingDependencies, exclusiveFormReadings, enrichFormMatches, expandFormActivations, settleFormActivations, rememberFormArguments, resetFormInputs, hasFormValue, formPath, formValuesEqual, formReadingReady, formReadingRelevant, formPreparation, queueFormReadings, pendingFormReadings, formWorkLimit, answerResetsFormReplays, rejectFormReadings, invalidateFormInputs, acceptFormReadings, formInstanceApplies, formParentResume, closeFormInstance, checkOtp, clearOtp, maskDestination, validateOTP, OTP_EXPIRY_MS, validateDigits, validatePhone, validateEmail, urlToSpeech, textWithUrlToSpeech };
|
|
256
|
+
export { continuationSafetyProtocol, cancelFormTask, contextualResponseProtocol, validateFormResponse, hasFormResponse, formProposalPolicy, stageFormProposal, nextFormProposal, formProposalReadings, bindFormProposal, decideFormProposal, commitFormProposals, settleFormProposalValidation, formProposalProtocol, validateFormProposalPolicy, validateFormProposalDescriptor, formProposalDescriptor, clearFormMisses, formNoOwner, missFormInput, formMustComplete, formInputPrompt, formAcknowledges, consumeFormFeedback, formConversationOutcome, readMoneyAmount, isMoneyAmount, moneyIntentAllows, isPastPaymentStatement, isPartialMoneyAnswer, validateMoneyOptions, takeChoiceOffer, saysConfirmWord, confirmOutOfOrderLine, matchesChoice, ANSWER_WORDS, CONFIRM_WORDS, isAnswer, isAgentRequest, isExitRequest, isOtherRequest, isSpelledFragment, isAmountAnswer, amountInAnswer, pickChoice, withoutPromptEcho, readEmail, readPhone, readInternationalPhone, readIdentifier, last4, maskEmail, callerTextNumber, textDestination, callerIsInternational, normalizeAndFindCapture, anythingElse, goodbye, sayName, sorryNotCaught, dateToWords, amountToSpeech, validateCardNumber, validateExpiration, consumeActionReceipt, authorizeFormActionRetry, formSignature, selectFormAction, confirmFormRevision, prepareFormAction, actionConfirmationValid, beginFormAction, finishFormAction, actionGateVerified, sourceNeedsLoad, beginSourceRefresh, completeSourceRefresh, pickProviderQuestion, matchInputs, readFormAnswer, readInputProfile, formAcceptanceTrace, resolveFormInputs, formInputApplies, formInputFilled, nextFormInput, formInputChain, openFormInstance, validateFormIncompleteOutcome, formIncompleteOutcome, validateFormExhaustion, exhaustFormInput, formInitialReadings, orderFormReadings, proposedFormValues, applyFormValidation, formReadingDependencies, exclusiveFormReadings, enrichFormMatches, expandFormActivations, settleFormActivations, rememberFormArguments, resetFormInputs, hasFormValue, formPath, formValuesEqual, formReadingReady, formReadingRelevant, formPreparation, queueFormReadings, pendingFormReadings, formWorkLimit, answerResetsFormReplays, rejectFormReadings, invalidateFormInputs, acceptFormReadings, formInstanceApplies, formParentResume, closeFormInstance, checkOtp, clearOtp, maskDestination, validateOTP, OTP_EXPIRY_MS, validateDigits, validatePhone, validateEmail, urlToSpeech, textWithUrlToSpeech };
|
|
253
257
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/helpers/index.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/helpers/index.js"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;2CACyC,iCAAiC;+BAI2F,wBAAwB;2CAH/G,0BAA0B;qCAA1B,0BAA0B;gCAA1B,0BAA0B;mCACsE,qBAAqB;kCAArB,qBAAqB;iCAArB,qBAAqB;qCAArB,qBAAqB;iCAArB,qBAAqB;mCAArB,qBAAqB;oCAArB,qBAAqB;6CAArB,qBAAqB;qCAC/E,wBAAwB;2CAAxB,wBAAwB;+CAAxB,wBAAwB;uCAAxB,wBAAwB;gCACyB,wBAAwB;4BAAxB,wBAAwB;8BAAxB,wBAAwB;iCAAxB,wBAAwB;gCAAxB,wBAAwB;iCAAxB,wBAAwB;oCAAxB,wBAAwB;wCAAxB,wBAAwB;gCAmB9D,YAAY;8BAAZ,YAAY;kCAAZ,YAAY;uCAAZ,YAAY;qCAAZ,YAAY;qCAAZ,YAAY;gCAlB3G,mBAAmB;gCA0BwE,mBAAmB;sCAAnB,mBAAmB;8BAThH,qBAAqB;6BAEoI,aAAa;8BAAb,aAAa;yBAAb,aAAa;+BAAb,aAAa;8BAAb,aAAa;+BAAb,aAAa;kCAAb,aAAa;+BAAb,aAAa;+BAAb,aAAa;2BAAb,aAAa;kCAAb,aAAa;0BAC3C,cAAc;0BAAd,cAAc;uCAAd,cAAc;+BAAd,cAAc;sBAAd,cAAc;0BAAd,cAAc;iCAAd,cAAc;gCAAd,cAAc;sCAAd,cAAc;wCAC/H,cAAc;6BACS,cAAc;wBAAd,cAAc;wBAAd,cAAc;+BAAd,cAAc;4BACjD,iBAAiB;+BACd,uBAAuB;mCACC,kBAAkB;mCAAlB,kBAAkB;qCAxB+I,mBAAmB;yCAAnB,mBAAmB;8BAAnB,mBAAmB;iCAAnB,mBAAmB;oCAAnB,mBAAmB;kCAAnB,mBAAmB;wCAAnB,mBAAmB;gCAAnB,mBAAmB;iCAAnB,mBAAmB;mCAAnB,mBAAmB;gCAe1I,mBAAmB;mCAAnB,mBAAmB;sCAAnB,mBAAmB;qCAAnB,mBAAmB;4BAUO,mBAAmB;+BAAnB,mBAAmB;iCAAnB,mBAAmB;oCAAnB,mBAAmB;kCAC0lB,iBAAiB;iCAAjB,iBAAiB;gCAAjB,iBAAiB;8BAAjB,iBAAiB;+BAAjB,iBAAiB;iCAAjB,iBAAiB;8CAAjB,iBAAiB;sCAAjB,iBAAiB;uCAAjB,iBAAiB;iCAAjB,iBAAiB;oCAAjB,iBAAiB;kCAAjB,iBAAiB;mCAAjB,iBAAiB;oCAAjB,iBAAiB;wCAAjB,iBAAiB;sCAAjB,iBAAiB;kCAAjB,iBAAiB;sCAAjB,iBAAiB;sCAAjB,iBAAiB;sCAAjB,iBAAiB;gCAAjB,iBAAiB;6BAAjB,iBAAiB;yBAAjB,iBAAiB;gCAAjB,iBAAiB;iCAAjB,iBAAiB;oCAAjB,iBAAiB;gCAAjB,iBAAiB;kCAAjB,iBAAiB;oCAAjB,iBAAiB;8BAAjB,iBAAiB;wCAAjB,iBAAiB;mCAAjB,iBAAiB;qCAAjB,iBAAiB;mCAAjB,iBAAiB;oCAAjB,iBAAiB;iCAAjB,iBAAiB;kCAAjB,iBAAiB;yBACzqB,gBAAgB;yBAAhB,gBAAgB;gCAAhB,gBAAgB;4BAAhB,gBAAgB;8BAAhB,gBAAgB;+BACnC,iBAAiB;8BAAjB,iBAAiB;8BAAjB,iBAAiB;4BAC7B,iBAAiB;oCAAjB,iBAAiB"}
|
package/dist/helpers/index.js
CHANGED
|
@@ -30,18 +30,18 @@ import { anythingElse, goodbye, sayName, sorryNotCaught } 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, formAcceptanceTrace } from './match-inputs.js';
|
|
33
|
+
import { matchInputs, readFormAnswer, saysConfirmWord, confirmOutOfOrderLine, 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, answerResetsFormReplays, rejectFormReadings, invalidateFormInputs, acceptFormReadings, formInstanceApplies, formParentResume, closeFormInstance } from './form-state.js';
|
|
35
35
|
import { checkOtp, clearOtp, maskDestination, validateOTP, OTP_EXPIRY_MS } from './otp-check.js';
|
|
36
36
|
import { validateDigits, validatePhone, validateEmail } from './validators.js';
|
|
37
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, formNoOwner, missFormInput, formMustComplete, formInputPrompt, formAcknowledges, consumeFormFeedback, formConversationOutcome, readMoneyAmount, isMoneyAmount, moneyIntentAllows, isPastPaymentStatement, isPartialMoneyAnswer, validateMoneyOptions, takeChoiceOffer, matchesChoice, ANSWER_WORDS, CONFIRM_WORDS, isAnswer, isAgentRequest, isExitRequest, isOtherRequest, isSpelledFragment, isAmountAnswer, amountInAnswer, pickChoice, withoutPromptEcho, readEmail, readPhone, readInternationalPhone, readIdentifier, last4, maskEmail, callerTextNumber, textDestination, callerIsInternational, normalizeAndFindCapture, anythingElse, goodbye, sayName, sorryNotCaught, dateToWords, amountToSpeech, validateCardNumber, validateExpiration, consumeActionReceipt, authorizeFormActionRetry, formSignature, selectFormAction, confirmFormRevision, prepareFormAction, actionConfirmationValid, beginFormAction, finishFormAction, actionGateVerified, sourceNeedsLoad, beginSourceRefresh, completeSourceRefresh, pickProviderQuestion, matchInputs, readFormAnswer, readInputProfile, formAcceptanceTrace, resolveFormInputs, formInputApplies, formInputFilled, nextFormInput, formInputChain, openFormInstance, validateFormIncompleteOutcome, formIncompleteOutcome, validateFormExhaustion, exhaustFormInput, formInitialReadings, orderFormReadings, proposedFormValues, applyFormValidation, formReadingDependencies, exclusiveFormReadings, enrichFormMatches, expandFormActivations, settleFormActivations, rememberFormArguments, resetFormInputs, hasFormValue, formPath, formValuesEqual, formReadingReady, formReadingRelevant, formPreparation, queueFormReadings, pendingFormReadings, formWorkLimit, answerResetsFormReplays, rejectFormReadings, invalidateFormInputs, acceptFormReadings, formInstanceApplies, formParentResume, closeFormInstance, checkOtp, clearOtp, maskDestination, validateOTP, OTP_EXPIRY_MS, validateDigits, validatePhone, validateEmail, urlToSpeech, textWithUrlToSpeech, };
|
|
38
|
+
export { continuationSafetyProtocol, cancelFormTask, contextualResponseProtocol, validateFormResponse, hasFormResponse, formProposalPolicy, stageFormProposal, nextFormProposal, formProposalReadings, bindFormProposal, decideFormProposal, commitFormProposals, settleFormProposalValidation, formProposalProtocol, validateFormProposalPolicy, validateFormProposalDescriptor, formProposalDescriptor, clearFormMisses, formNoOwner, missFormInput, formMustComplete, formInputPrompt, formAcknowledges, consumeFormFeedback, formConversationOutcome, readMoneyAmount, isMoneyAmount, moneyIntentAllows, isPastPaymentStatement, isPartialMoneyAnswer, validateMoneyOptions, takeChoiceOffer, saysConfirmWord, confirmOutOfOrderLine, matchesChoice, ANSWER_WORDS, CONFIRM_WORDS, isAnswer, isAgentRequest, isExitRequest, isOtherRequest, isSpelledFragment, isAmountAnswer, amountInAnswer, pickChoice, withoutPromptEcho, readEmail, readPhone, readInternationalPhone, readIdentifier, last4, maskEmail, callerTextNumber, textDestination, callerIsInternational, normalizeAndFindCapture, anythingElse, goodbye, sayName, sorryNotCaught, dateToWords, amountToSpeech, validateCardNumber, validateExpiration, consumeActionReceipt, authorizeFormActionRetry, formSignature, selectFormAction, confirmFormRevision, prepareFormAction, actionConfirmationValid, beginFormAction, finishFormAction, actionGateVerified, sourceNeedsLoad, beginSourceRefresh, completeSourceRefresh, pickProviderQuestion, matchInputs, readFormAnswer, readInputProfile, formAcceptanceTrace, resolveFormInputs, formInputApplies, formInputFilled, nextFormInput, formInputChain, openFormInstance, validateFormIncompleteOutcome, formIncompleteOutcome, validateFormExhaustion, exhaustFormInput, formInitialReadings, orderFormReadings, proposedFormValues, applyFormValidation, formReadingDependencies, exclusiveFormReadings, enrichFormMatches, expandFormActivations, settleFormActivations, rememberFormArguments, resetFormInputs, hasFormValue, formPath, formValuesEqual, formReadingReady, formReadingRelevant, formPreparation, queueFormReadings, pendingFormReadings, formWorkLimit, answerResetsFormReplays, rejectFormReadings, invalidateFormInputs, acceptFormReadings, formInstanceApplies, formParentResume, closeFormInstance, checkOtp, clearOtp, maskDestination, validateOTP, OTP_EXPIRY_MS, validateDigits, validatePhone, validateEmail, urlToSpeech, textWithUrlToSpeech, };
|
|
39
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,
|
|
42
42
|
formProposalPolicy, stageFormProposal, nextFormProposal, formProposalReadings, bindFormProposal, decideFormProposal, commitFormProposals, settleFormProposalValidation, formProposalProtocol, validateFormProposalPolicy, validateFormProposalDescriptor, formProposalDescriptor,
|
|
43
43
|
clearFormMisses, formNoOwner, missFormInput, formMustComplete, formInputPrompt, formAcknowledges, consumeFormFeedback, formConversationOutcome,
|
|
44
|
-
readMoneyAmount, isMoneyAmount, moneyIntentAllows, isPastPaymentStatement, isPartialMoneyAnswer, validateMoneyOptions, takeChoiceOffer, matchesChoice, isAnswer, isAgentRequest, isExitRequest, isOtherRequest, isSpelledFragment, isAmountAnswer, amountInAnswer, pickChoice, withoutPromptEcho,
|
|
44
|
+
readMoneyAmount, isMoneyAmount, moneyIntentAllows, isPastPaymentStatement, isPartialMoneyAnswer, validateMoneyOptions, takeChoiceOffer, saysConfirmWord, confirmOutOfOrderLine, matchesChoice, isAnswer, isAgentRequest, isExitRequest, isOtherRequest, isSpelledFragment, isAmountAnswer, amountInAnswer, pickChoice, withoutPromptEcho,
|
|
45
45
|
readEmail, readPhone, readInternationalPhone, readIdentifier, last4, maskEmail, callerTextNumber, textDestination, callerIsInternational,
|
|
46
46
|
normalizeAndFindCapture, anythingElse, goodbye, sayName, sorryNotCaught, dateToWords, amountToSpeech, validateCardNumber, validateExpiration, consumeActionReceipt, authorizeFormActionRetry, formSignature, selectFormAction, confirmFormRevision, prepareFormAction, actionConfirmationValid, beginFormAction, finishFormAction, actionGateVerified, sourceNeedsLoad, beginSourceRefresh, completeSourceRefresh, pickProviderQuestion, matchInputs, readFormAnswer, readInputProfile, formAcceptanceTrace, resolveFormInputs, formInputApplies, formInputFilled, nextFormInput, formInputChain, openFormInstance, validateFormIncompleteOutcome, formIncompleteOutcome, validateFormExhaustion, exhaustFormInput, formInitialReadings, orderFormReadings, proposedFormValues, applyFormValidation, formReadingDependencies, exclusiveFormReadings, enrichFormMatches, expandFormActivations, settleFormActivations, rememberFormArguments, resetFormInputs, hasFormValue, formPath, formValuesEqual, formReadingReady, formReadingRelevant, formPreparation, queueFormReadings, pendingFormReadings, formWorkLimit, answerResetsFormReplays, rejectFormReadings, invalidateFormInputs, acceptFormReadings, formInstanceApplies, formParentResume, closeFormInstance,
|
|
47
47
|
validateOTP, validateDigits, validatePhone, validateEmail, urlToSpeech, textWithUrlToSpeech,
|
|
@@ -22,6 +22,7 @@ export function readInputProfile(profile: any, answer: any, isCurrent?: boolean)
|
|
|
22
22
|
* 2. else the one input of the current form that fits takes it, and parent forms are not considered;
|
|
23
23
|
* 3. else the same test one form up, and so on;
|
|
24
24
|
* 4. two inputs at the nearest form that fits are ambiguous: asked, never ranked.
|
|
25
|
+
* Exception: a number that fits two inputs out of order is asked about wherever they are (owner 2026-11-02).
|
|
25
26
|
* Exception: an input that declares `confirm_against: [names]` is asked about together with those inputs whenever both
|
|
26
27
|
* fit the same words, wherever scope would have put it ('62' at the payment offer: $62 or sub-account 62, prod
|
|
27
28
|
* 2026-10-19 07:38).
|
|
@@ -32,11 +33,14 @@ export function readInputProfile(profile: any, answer: any, isCurrent?: boolean)
|
|
|
32
33
|
*/
|
|
33
34
|
/** Explicit information questions are a separate answer kind, so their topic is not accepted as a field value.
|
|
34
35
|
* A question clause is consumed as a question; independently separated clauses remain available for collection. */
|
|
36
|
+
export function saysConfirmWord(answer: any, spec: any): boolean;
|
|
37
|
+
export function confirmOutOfOrderLine(spec: any, value: any, language: any, voice: any): string;
|
|
35
38
|
export function readFormAnswer(answer: any, candidates: any, current: any, choiceKey: any, questions?: any[]): {
|
|
36
39
|
hits: any[];
|
|
37
40
|
ambiguous: any;
|
|
38
41
|
remaining: any[];
|
|
39
42
|
questions: any[];
|
|
43
|
+
clarify?: any[] | undefined;
|
|
40
44
|
decisions?: {
|
|
41
45
|
form: any;
|
|
42
46
|
name: any;
|
|
@@ -55,6 +59,7 @@ export function readFormAnswer(answer: any, candidates: any, current: any, choic
|
|
|
55
59
|
})[] | undefined;
|
|
56
60
|
};
|
|
57
61
|
export function matchInputs(answer: any, candidates: any, current: any, choiceKey: any): {
|
|
62
|
+
clarify?: any[] | undefined;
|
|
58
63
|
decisions?: {
|
|
59
64
|
form: any;
|
|
60
65
|
name: any;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"match-inputs.d.ts","sourceRoot":"","sources":["../../src/helpers/match-inputs.js"],"names":[],"mappings":"AAiBA;;;;GAIG;AACH,sEAQC;
|
|
1
|
+
{"version":3,"file":"match-inputs.d.ts","sourceRoot":"","sources":["../../src/helpers/match-inputs.js"],"names":[],"mappings":"AAiBA;;;;GAIG;AACH,sEAQC;AAwFD;;;GAGG;AACH,sFA8GC;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH;mHACmH;AAEnH,iEAEC;AAGD,gGAKC;AAED;;;;;;;;;;;;;;;;;;;;;;EAeC;AACD;;;;;;;;;;;;;;;;;;;;;;;EA6RC"}
|
|
@@ -56,7 +56,8 @@ function amountMasked(raw) {
|
|
|
56
56
|
}
|
|
57
57
|
/** Source intervals let different readers recognize the same evidence without agreeing on its type. */
|
|
58
58
|
function numberSpans(raw, value, amount = false) {
|
|
59
|
-
|
|
59
|
+
// A field's number is located as its reader found it: DIGIT_RUN joins "31, 0 31". An unlocated number escaped every span rule.
|
|
60
|
+
const runs = [...raw.matchAll(amount ? /\d(?:[\s.,-]?\d)*/g : DIGIT_RUN)];
|
|
60
61
|
if (!amount) {
|
|
61
62
|
const wanted = String(value).replace(/\D/g, '');
|
|
62
63
|
return runs.filter(m => { const d = m[0].replace(/\D/g, ''); return d === wanted || d === '1' + wanted; })
|
|
@@ -271,6 +272,7 @@ export function readInputProfile(profile, answer, isCurrent = true) {
|
|
|
271
272
|
* 2. else the one input of the current form that fits takes it, and parent forms are not considered;
|
|
272
273
|
* 3. else the same test one form up, and so on;
|
|
273
274
|
* 4. two inputs at the nearest form that fits are ambiguous: asked, never ranked.
|
|
275
|
+
* Exception: a number that fits two inputs out of order is asked about wherever they are (owner 2026-11-02).
|
|
274
276
|
* Exception: an input that declares `confirm_against: [names]` is asked about together with those inputs whenever both
|
|
275
277
|
* fit the same words, wherever scope would have put it ('62' at the payment offer: $62 or sub-account 62, prod
|
|
276
278
|
* 2026-10-19 07:38).
|
|
@@ -281,6 +283,17 @@ export function readInputProfile(profile, answer, isCurrent = true) {
|
|
|
281
283
|
*/
|
|
282
284
|
/** Explicit information questions are a separate answer kind, so their topic is not accepted as a field value.
|
|
283
285
|
* A question clause is consumed as a question; independently separated clauses remain available for collection. */
|
|
286
|
+
// confirm_out_of_order's word, in either language, said in the answer.
|
|
287
|
+
export function saysConfirmWord(answer, spec) {
|
|
288
|
+
return [spec?.word, spec?.word_es].some((word) => word && matchesChoice(String(answer ?? ''), [String(word).toLowerCase()]));
|
|
289
|
+
}
|
|
290
|
+
// confirm_out_of_order's line: {value} the number (digit by digit on a call), {word} its word.
|
|
291
|
+
export function confirmOutOfOrderLine(spec, value, language, voice) {
|
|
292
|
+
const es = language === 'es';
|
|
293
|
+
const line = String((es && spec?.message_es) || spec?.message || '');
|
|
294
|
+
const word = String((es && spec?.word_es) || spec?.word || '');
|
|
295
|
+
return line.replace('{value}', String(value ?? '').split('').join(voice ? ' ' : '')).replace('{word}', word);
|
|
296
|
+
}
|
|
284
297
|
export function readFormAnswer(answer, candidates, current, choiceKey, questions = []) {
|
|
285
298
|
const raw = unescape(answer), parts = raw.split(/([?;\n]|\b(?:and|y)\b)/i), asked = [];
|
|
286
299
|
let consumed = false;
|
|
@@ -368,7 +381,7 @@ export function matchInputs(answer, candidates, current, choiceKey) {
|
|
|
368
381
|
// A declared confirm_against pair with the current question is relevant to it as well: the pair is asked about.
|
|
369
382
|
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)));
|
|
370
383
|
const inOrder = (reading) => reading.current || invited(reading) || pairedWithCurrent(reading);
|
|
371
|
-
// Rule 3: a number read out of order
|
|
384
|
+
// Rule 3, the whole number: every digit run a number read out of order touches lies entirely inside it,
|
|
372
385
|
// so the last four of "88 91 7112" fill nothing. A written decimal is a run of its own ("51 72.45": 51, then 72.45).
|
|
373
386
|
const decimalRuns = [...raw.matchAll(DECIMAL_AMOUNT)].map((m) => [m.index, m.index + m[0].length]);
|
|
374
387
|
const undecimal = decimalRuns.reduce((text, [a, b]) => text.slice(0, a) + ' '.repeat(b - a) + text.slice(b), raw);
|
|
@@ -450,6 +463,37 @@ export function matchInputs(answer, candidates, current, choiceKey) {
|
|
|
450
463
|
}
|
|
451
464
|
// Rule 4: a single digit the current question offers as a key is that key (the keypad reading wins its group below);
|
|
452
465
|
// one it does not offer reaches the invited amount, whose own validation decides ("3 dollars?").
|
|
466
|
+
// Rule 3, a perfect match (owner 2026-11-02): a number read out of order accounts for every digit of the answer. "0 4 guion
|
|
467
|
+
// 31, 0 31" at the expiration is not a ZIP 31031 and "31st. 9 3 1." not a code 931. Several numbers are a compound answer only
|
|
468
|
+
// when each is read, and a free-text answer owns its own digits. An amount has rule 4, a choice its declared phrases.
|
|
469
|
+
const readsDigits = (reading) => !['choice', 'amount', 'text'].includes(fieldOf(reading).profile.kind) && /\d/.test(spanText(reading) + String(reading.value));
|
|
470
|
+
const outOfOrderDigits = readings.filter((reading) => reading.rule === 'out_of_order' && readsDigits(reading));
|
|
471
|
+
const unread = unclaimedSpans(raw, readings.filter((reading) => reading.unlocated !== true)).map(([a, b]) => raw.slice(a, b)).join(' ');
|
|
472
|
+
if (outOfOrderDigits.length && /\d/.test(unread)) {
|
|
473
|
+
for (const reading of outOfOrderDigits)
|
|
474
|
+
refused.push({ form: reading.form, name: reading.name, value: reading.value, rule: 'not_whole_answer', spans: reading.spans });
|
|
475
|
+
readings = readings.filter((reading) => !outOfOrderDigits.includes(reading));
|
|
476
|
+
}
|
|
477
|
+
else {
|
|
478
|
+
// no number read out of order, or nothing of the answer's digits is left over
|
|
479
|
+
}
|
|
480
|
+
// input.confirm_out_of_order {word, word_es, message, message_es}: out of order a number fills this input only beside its
|
|
481
|
+
// word ("account 5373850"). Alone it may be anything (a code said with an extra digit, owner 2026-11-02), so the caller is
|
|
482
|
+
// told how to mean it and the current question is asked again.
|
|
483
|
+
const clarify = [];
|
|
484
|
+
readings = readings.filter((reading) => {
|
|
485
|
+
const field = fieldOf(reading);
|
|
486
|
+
if (reading.rule !== 'out_of_order' || !field.confirm_out_of_order || saysConfirmWord(raw, field.confirm_out_of_order)) {
|
|
487
|
+
return true;
|
|
488
|
+
}
|
|
489
|
+
else {
|
|
490
|
+
refused.push({ form: reading.form, name: reading.name, value: reading.value, rule: 'confirm_out_of_order', spans: reading.spans });
|
|
491
|
+
const taken = readings.some((other) => inOrder(other) && !other.fallback && (other.spans || []).some(([s, e]) => (reading.spans || []).some(([t, u]) => s < u && t < e)));
|
|
492
|
+
if (!taken)
|
|
493
|
+
clarify.push({ form: reading.form, name: reading.name, value: reading.value, ...field.confirm_out_of_order });
|
|
494
|
+
return false;
|
|
495
|
+
}
|
|
496
|
+
});
|
|
453
497
|
// profile.whole_phrases (a choice, or a pattern that reads words): a phrase picks only when the answer is accounted
|
|
454
498
|
// for: its phrases, the arguments read beside them (numbers, amounts, a card), courtesy or connecting words
|
|
455
499
|
// (isReaderFiller) and the field's own profile.template_words. A template such as "pay 50 with my Visa card" stays;
|
|
@@ -537,8 +581,9 @@ export function matchInputs(answer, candidates, current, choiceKey) {
|
|
|
537
581
|
winners = members.filter((m) => m.current);
|
|
538
582
|
}
|
|
539
583
|
else {
|
|
584
|
+
// A number that fits two inputs out of order is asked about wherever they are (owner 2026-11-02); scope settles words.
|
|
540
585
|
const nearest = Math.min(...members.map((m) => m.level));
|
|
541
|
-
winners = members.filter((m) => m.level === nearest);
|
|
586
|
+
winners = members.every(readsDigits) ? members : members.filter((m) => m.level === nearest);
|
|
542
587
|
}
|
|
543
588
|
// a winner declared to be confirmed against another member is asked about together with it
|
|
544
589
|
const confirmWith = winners.length === 1 ? members.filter((m) => m !== winners[0] && paired(m, winners[0])) : [];
|
|
@@ -577,6 +622,6 @@ export function matchInputs(answer, candidates, current, choiceKey) {
|
|
|
577
622
|
return { hits: [], ambiguous, refused: [...stopped, ...hits.map((hit) => ({ form: hit.form, name: hit.name, value: hit.value, rule: 'unaccounted_words', spans: hit.spans }))], ...(decisions.length ? { decisions } : {}) };
|
|
578
623
|
}
|
|
579
624
|
else {
|
|
580
|
-
return { hits, ambiguous, ...(remaining.length ? { remaining } : {}), ...(stopped.length ? { refused: stopped } : {}), ...(decisions.length ? { decisions } : {}) };
|
|
625
|
+
return { hits, ambiguous, ...(remaining.length ? { remaining } : {}), ...(stopped.length ? { refused: stopped } : {}), ...(decisions.length ? { decisions } : {}), ...(clarify.length ? { clarify } : {}) };
|
|
581
626
|
}
|
|
582
627
|
}
|
package/dist/runtime-identity.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const JSFE_RUNTIME_FINGERPRINT = "sha256:
|
|
1
|
+
export const JSFE_RUNTIME_FINGERPRINT = "sha256:541d4fe4c745f7cd5a687cb3bcdf56d15f3aaa355b5821be2e0e649716a14689";
|
|
@@ -278,6 +278,8 @@ does this reply do with the question the flow asked? The model picks from a clos
|
|
|
278
278
|
|
|
279
279
|
**Word readers and the ranker.** An input with `profile.whole_phrases` reads a choice from words only when the whole reply is accounted for: its phrase, another input's argument, courtesy, connecting words and the input's `template_words`. A reply with any other word goes to the ranker, so a word inside a sentence ("Quiero hacer una nueva compra" at a pay menu) never picks an option. Compound answers that carry arguments stay with the reader ("pay $50 with my Visa card"), because a picked option extracts no arguments. A proposal's confirmation offers the ranker its input's choices, so a correction ("yes, it was stolen") picks the changed candidate.
|
|
280
280
|
|
|
281
|
+
**A number that must name itself.** `input.confirm_out_of_order: {word, word_es, message, message_es}` (default off): out of order a number fills this input only beside its word, in either language ("account 5373850"). Alone it may be something else, a code said with an extra digit, so nothing is filled: the caller hears `message` (`{value}` the number, digit by digit on a call; `{word}` the word) and the current question again, which counts as an attempt at that question. It works at `collect-input` and `confirm-form` questions. At its own question the number needs no word. `authenticate-user`'s `code_confirm` takes the same declaration for a `code_validator`, whose flow reads it with the helpers `saysConfirmWord(answer, spec)` and `confirmOutOfOrderLine(spec, value, language, voice)`.
|
|
282
|
+
|
|
281
283
|
**A provider's menu.** `provides.menu.whole_phrases: true` (default off) applies the same rule to a menu: a fact's phrase answers only as the whole reply, and a phrase inside a longer sentence goes to the reply ranker, offered every fact with phrases, every action, `done` and a person. A fact pick is answered with every other fact that cleared `REPLY_ALSO_CUT` (0.6), so one sentence that asks two things hears both; an action pick runs alone. `answers` and `irrelevant` fall to the menu's plain DISPATCH, which hands the turn to the host without intent detection (a Guru answers from the user context the flow set); `new_request` runs intent detection. A reply with no phrase is unchanged. `menu.unanswered` handles a question the provider cannot answer: when a reply from its own menu comes back to it through intent detection with no fact to say, the caller hears the declared apology and offer (`provider-unanswered`) instead of the same facts again.
|
|
282
284
|
|
|
283
285
|
Some options are **never offered**, so the ranker cannot pick them: the YES of a firm confirmation or of a form's
|
|
@@ -217,6 +217,7 @@ Declarations:
|
|
|
217
217
|
| input `confirmation:"firm"` / `"soft"` | A yes/no decision. Firm (consent to a charge, a cancellation, storing a card, filing): YES only by `CONFIRM_WORDS` or the option's own exact phrases. Soft (an offer): the wider YES vocabulary. NO by the NO vocabulary or the option's own phrases. |
|
|
218
218
|
| input `invites:[names]`, `form.confirm.invites` | The other inputs this question (or the read-back) also asks for ("NEW CARD, LINK, or a different amount" invites `amount`), in its form or an enclosing one; read as in order while it is asked. |
|
|
219
219
|
| profile `out_of_order:{…}` | A narrower signature for out-of-order reading: the payment card takes a bare last four when asked, but out of order only a card word or "ending in 4242". |
|
|
220
|
+
| input `confirm_out_of_order:{word,word_es,message,message_es}` | Out of order a number fills this input only beside its `word`, in either language. Alone, the caller hears `message` (`{value}` is the number, `{word}` the word) and the current question again, which counts as an attempt. `authenticate-user`'s `code_confirm` is the same declaration for a `code_validator`, whose flow reads it with `saysConfirmWord` and `confirmOutOfOrderLine`: an account number at the verification-code question, where it may be a code said with an extra digit. |
|
|
220
221
|
| input `choices_from` | The choice's values come from prepared data (the caller's saved cards), so its signature is those values. |
|
|
221
222
|
|
|
222
223
|
Rules, in order:
|
|
@@ -232,16 +233,21 @@ Rules, in order:
|
|
|
232
233
|
`depends_on` prerequisites are not filled yet (the sub-account before the account is known); a named answer ("50
|
|
233
234
|
dollars") still waits as a pending value. **The whole signature:** A digit field takes a number only when the whole number fits its
|
|
234
235
|
length; a pattern only when it covers the whole number; a choice only its declared or prepared values. A fragment of
|
|
235
|
-
a longer number (the last four of "88 91 7112") fills nothing.
|
|
236
|
+
a longer number (the last four of "88 91 7112") fills nothing. **A perfect match:** a number read out of order accounts for every digit
|
|
237
|
+
of the answer, alone or with the other fields a compound answer fills. "0 4 guion 31, 0 31" at the expiration fills no ZIP;
|
|
238
|
+
"the line is 8185551234" fills the phone. Its validation decides too: a number its validator rejects fills nothing and says nothing, and the question
|
|
239
|
+
is asked again.
|
|
236
240
|
4. **A bare number** (no money mark, no word naming a field) is an amount only at a question that is, invites or is
|
|
237
241
|
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,
|
|
238
|
-
whose validation decides
|
|
242
|
+
whose validation decides. The engine holds no amounts, so "only a known amount" is the amount validator's rule: Curacao's
|
|
243
|
+
takes a bare number at the amount question, and at a question that only invites an amount only when it is an amount the
|
|
244
|
+
caller can pay (else it asks for it in dollars). A bare number that does not fit the current question is
|
|
239
245
|
read out of order under rule 3.
|
|
240
246
|
5. **Compound answers.** Several fields fill from one answer only through the form's compound parser, or when every
|
|
241
247
|
part of the answer fits exactly one field. **One part of an answer never fills two fields**; the engine refuses that
|
|
242
248
|
reading and records the refusal.
|
|
243
249
|
6. **Scope.** Across forms the nearest form wins; two fields of the same form that fit the same part are asked about,
|
|
244
|
-
never ranked.
|
|
250
|
+
never ranked. A number that fits two fields out of order is asked about wherever they are.
|
|
245
251
|
|
|
246
252
|
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
|
|
247
253
|
refused reading is listed with its rule, so hosts can log what was filled out of order and what was refused.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jsfe",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.31.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",
|