jsfe 1.15.7 → 1.15.9
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/dist/forms/declarations.js +1 -1
- package/dist/forms/flows.d.ts +2 -1
- package/dist/forms/flows.d.ts.map +1 -1
- package/dist/forms/flows.js +22 -11
- package/dist/forms/identity-flows.d.ts.map +1 -1
- package/dist/forms/identity-flows.js +4 -1
- package/dist/helpers/answer.d.ts +2 -0
- package/dist/helpers/answer.d.ts.map +1 -1
- package/dist/helpers/answer.js +5 -0
- package/dist/helpers/form-conversation.d.ts +3 -0
- package/dist/helpers/form-conversation.d.ts.map +1 -1
- package/dist/helpers/form-conversation.js +18 -0
- package/dist/helpers/form-state.d.ts +2 -1
- package/dist/helpers/form-state.d.ts.map +1 -1
- package/dist/helpers/form-state.js +6 -2
- package/dist/helpers/index.d.ts +3 -1
- package/dist/helpers/index.d.ts.map +1 -1
- package/dist/helpers/index.js +3 -3
- package/dist/helpers/match-inputs.d.ts.map +1 -1
- package/dist/helpers/match-inputs.js +58 -11
- package/dist/helpers/money.d.ts.map +1 -1
- package/dist/helpers/money.js +5 -2
- package/dist/host/intent-stages.d.ts +6 -1
- package/dist/host/intent-stages.d.ts.map +1 -1
- package/dist/host/intent-stages.js +10 -2
- package/dist/runtime-identity.js +1 -1
- package/docs/copilot-reference.md +3 -0
- package/package.json +1 -1
|
@@ -4,7 +4,7 @@ import { linkFormSchemas, validateFlowContracts } from './contracts.js';
|
|
|
4
4
|
import { compileFormPrompts } from './prompt-compiler.js';
|
|
5
5
|
import { withSafetyRevision } from './continuation-safety.js';
|
|
6
6
|
import { capturedAuthorSpec, copyAuthorSpec, authoredExpressions } from './declaration-capture.js';
|
|
7
|
-
const formKeys = new Set(['inputs', 'before', 'after', 'confirm', 'before_confirm', 'declined', 'stop', 'close_on_stop', 'resume_on_parent_change', 'done', 'validator', 'confirmation_condition', 'prompt', 'prompt_es', 'incomplete_outcome', 'on_exhausted', 'must_complete']);
|
|
7
|
+
const formKeys = new Set(['inputs', 'before', 'after', 'confirm', 'before_confirm', 'declined', 'stop', 'close_on_stop', 'resume_on_parent_change', 'done', 'validator', 'confirmation_condition', 'prompt', 'prompt_es', 'incomplete_outcome', 'on_exhausted', 'on_no', 'must_complete']);
|
|
8
8
|
const sharedKeys = ['id', 'name', 'description', 'version', 'scope', 'primary', 'parameters', 'contract', 'parameterInstructions'];
|
|
9
9
|
const safe = /^[A-Za-z_$][\w$]*$/;
|
|
10
10
|
const fail = message => { throw new Error('Flow declaration: ' + message); };
|
package/dist/forms/flows.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export function formFlow({ id, name, version, description, inputs, after, extraVars, parameters, confirm, before_confirm, declined, stop, close_on_stop, resume_on_parent_change, before, done, primary, scope, validator, confirmation_condition, prompt, prompt_es, incomplete_outcome, on_exhausted, must_complete }: {
|
|
1
|
+
export function formFlow({ id, name, version, description, inputs, after, extraVars, parameters, confirm, before_confirm, declined, stop, close_on_stop, on_no, resume_on_parent_change, before, done, primary, scope, validator, confirmation_condition, prompt, prompt_es, incomplete_outcome, on_exhausted, must_complete }: {
|
|
2
2
|
id: any;
|
|
3
3
|
name: any;
|
|
4
4
|
version: any;
|
|
@@ -12,6 +12,7 @@ export function formFlow({ id, name, version, description, inputs, after, extraV
|
|
|
12
12
|
declined?: never[] | undefined;
|
|
13
13
|
stop?: string | undefined;
|
|
14
14
|
close_on_stop?: boolean | undefined;
|
|
15
|
+
on_no?: string | undefined;
|
|
15
16
|
resume_on_parent_change?: boolean | undefined;
|
|
16
17
|
before?: never[] | undefined;
|
|
17
18
|
done?: boolean | undefined;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"flows.d.ts","sourceRoot":"","sources":["../../src/forms/flows.js"],"names":[],"mappings":"AAqIA
|
|
1
|
+
{"version":3,"file":"flows.d.ts","sourceRoot":"","sources":["../../src/forms/flows.js"],"names":[],"mappings":"AAqIA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA6DC;AACD;;;GAGG;AACH;;;;;;EAGC;AAgfD,wHAAwH;AACxH,wEAaC;AACD,4FAA4F;AAC5F;;;EAYC;AACD,sGAAsG;AACtG;;;EAOC;AArrBM,6CAAuG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA82B9G;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAOG"}
|
package/dist/forms/flows.js
CHANGED
|
@@ -127,8 +127,10 @@ function validateAcceptanceDeclarations(id, inputs) {
|
|
|
127
127
|
else { /* no invites, or a list of names */ }
|
|
128
128
|
}
|
|
129
129
|
}
|
|
130
|
-
export function formFlow({ id, name, version, description, inputs, after = [], extraVars = {}, parameters = [], confirm = null, before_confirm = [], declined = [], stop = '', close_on_stop = false, resume_on_parent_change = false, before = [], done = true, primary = false, scope = 'isolated', validator = '', confirmation_condition = 'true', prompt = undefined, prompt_es = undefined, incomplete_outcome = null, on_exhausted = null, must_complete = false }) {
|
|
130
|
+
export function formFlow({ id, name, version, description, inputs, after = [], extraVars = {}, parameters = [], confirm = null, before_confirm = [], declined = [], stop = '', close_on_stop = false, on_no = '', resume_on_parent_change = false, before = [], done = true, primary = false, scope = 'isolated', validator = '', confirmation_condition = 'true', prompt = undefined, prompt_es = undefined, incomplete_outcome = null, on_exhausted = null, must_complete = false }) {
|
|
131
131
|
validateFormExhaustion(on_exhausted);
|
|
132
|
+
if (on_no !== '' && (typeof on_no !== 'string' || !/^[\w-]+$/.test(on_no)))
|
|
133
|
+
throw new Error(`FORM ${id}: on_no must be a flow id`);
|
|
132
134
|
if (typeof must_complete !== 'boolean')
|
|
133
135
|
throw new Error(`FORM ${id}: must_complete must be true or false`);
|
|
134
136
|
validateAcceptanceDeclarations(id, inputs);
|
|
@@ -154,13 +156,13 @@ export function formFlow({ id, name, version, description, inputs, after = [], e
|
|
|
154
156
|
] : [];
|
|
155
157
|
return {
|
|
156
158
|
id, name, version, description, scope, metadata: { formProtocol: 2 }, parameters: [...parameters, { name: 'form_instance', type: 'string', internal: true, description: 'Internal instance to resume' }], ...(primary ? { primary: true } : {}), ...(prompt ? { prompt, prompt_es } : {}),
|
|
157
|
-
variables: vars({ form_on_exhausted: on_exhausted, form_must_complete: must_complete, form_incomplete_outcome: incompleteFrom ? null : incomplete_outcome, form_instance: '', form_internal_resume: false, form_resume_owner: '', form_arguments: {}, form_exhaustion: null, form_values: {}, form_validation: '', initial_readings: [], inputs: resolveFormInputs(inputs, FIELD_PRESETS), parent_form: '', result_input: '', questions: [], provides: null, exit_context: '', exit_context_es: '', form_confirm_q: '', form_confirm_q_es: '', form_confirmation: confirm || {}, form_should_confirm: false, ...extraVars }),
|
|
159
|
+
variables: vars({ form_on_exhausted: on_exhausted, form_on_no: on_no, form_must_complete: must_complete, form_incomplete_outcome: incompleteFrom ? null : incomplete_outcome, form_instance: '', form_internal_resume: false, form_resume_owner: '', form_arguments: {}, form_exhaustion: null, form_values: {}, form_validation: '', initial_readings: [], inputs: resolveFormInputs(inputs, FIELD_PRESETS), parent_form: '', result_input: '', questions: [], provides: null, exit_context: '', exit_context_es: '', form_confirm_q: '', form_confirm_q_es: '', form_confirmation: confirm || {}, form_should_confirm: false, ...extraVars }),
|
|
158
160
|
steps: [
|
|
159
161
|
...contextualResponseRuntimeSteps(id, [], hasFormResponse(inputs) ? [CONTEXTUAL_RESPONSES_REQUIREMENT] : []),
|
|
160
162
|
...(hasFormResponse(inputs) ? [{ ...SET(`${id}_response_reader_required`, 'noop', "typeof validateFormResponse === 'function'"), requireTrue: true }] : []),
|
|
161
163
|
...proposalRuntimeSteps(id, [], hasInputProposals(inputs) ? [INPUT_PROPOSALS_REQUIREMENT] : []),
|
|
162
164
|
SET(`${id}_resume_kind`, 'form_internal_resume', '!!form_instance'),
|
|
163
|
-
SET(`${id}_open`, 'form_instance', `openFormInstance(cargo, ${JSON.stringify(id)}, inputs, { parent: parent_form || null, input: result_input || '', context:!!parent_form && !result_input, resume_on_parent_change: ${resume_on_parent_change === true}, resume: form_instance || (!parent_form ? (cargo.formTasks || {})[${JSON.stringify(id)}] || '' : ''), providers: provides, questions: questions, incomplete_outcome: ${incompleteExpression}, on_exhausted: form_on_exhausted, must_complete: form_must_complete })`),
|
|
165
|
+
SET(`${id}_open`, 'form_instance', `openFormInstance(cargo, ${JSON.stringify(id)}, inputs, { parent: parent_form || null, input: result_input || '', context:!!parent_form && !result_input, resume_on_parent_change: ${resume_on_parent_change === true}, resume: form_instance || (!parent_form ? (cargo.formTasks || {})[${JSON.stringify(id)}] || '' : ''), providers: provides, questions: questions, incomplete_outcome: ${incompleteExpression}, on_exhausted: form_on_exhausted, on_no: form_on_no, must_complete: form_must_complete })`),
|
|
164
166
|
SIDE(`${id}_task`, `(!parent_form ? (cargo.formTasks = cargo.formTasks || {}, cargo.formTasks[${JSON.stringify(id)}] = form_instance) : true, true)`),
|
|
165
167
|
SIDE(`${id}_provider_reset`, '(delete cargo.forms[form_instance].providerRequests, delete cargo.forms[form_instance].activeGates, delete cargo.forms[form_instance].providerGateActive, true)'),
|
|
166
168
|
SIDE(`${id}_pass_reset`, '(cargo.forms[form_instance].replays = 0, form_internal_resume ? true : (cargo.forms[form_instance].attempts = {},cargo.forms[form_instance].misses={},cargo.forms[form_instance].feedback={}), true)'),
|
|
@@ -250,7 +252,7 @@ const collectInput = {
|
|
|
250
252
|
id: 'collect-input', name: 'CollectInput', version: '1.0.0', scope: 'isolated',
|
|
251
253
|
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).",
|
|
252
254
|
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 })),
|
|
253
|
-
variables: vars({ ci_partial: '', 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: '' }),
|
|
255
|
+
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: '' }),
|
|
254
256
|
steps: [
|
|
255
257
|
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; })()"),
|
|
256
258
|
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]"),
|
|
@@ -299,6 +301,15 @@ const collectInput = {
|
|
|
299
301
|
// short of the input's minimum the next question is "Go on." Anything that is not digits drops the partial.
|
|
300
302
|
SET('ci_partial_join', 'ci_partial', "(function () { var state = cargo.forms[form]; if (!ci_spec || ci_spec.accumulate !== true || !ci_spec.digits) return ''; var said = String(cargo.choiceText || '').trim(); var data = state.data = state.data || {}; var key = '__partial_' + current; if (!/^[0-9][0-9\\s.,-]*$/.test(said)) { delete data[key]; return ''; } var joined = String(data[key] || '') + said.replace(/[^0-9]/g, ''); var starts = ci_spec.accumulate_first ? new RegExp(ci_spec.accumulate_first) : null; if (!data[key] && starts && !starts.test(joined)) return ''; if (joined.length < Number(ci_spec.digits.min || 0)) { data[key] = joined; var fb = state.feedback || {}; fb[current] = 'partial'; state.feedback = fb; return 'hold'; } delete data[key]; cargo.choiceText = joined; cargo.choice = joined; return 'joined'; })()"),
|
|
301
303
|
WHEN('ci_partial_hold', "ci_partial === 'hold'", END('ci_partial_wait')),
|
|
304
|
+
// A whole NO at a question that does not read it, in a form (or a sub-form of one) that declares `on_no`: that
|
|
305
|
+
// form's worker answers it (a payment starts its selection again), instead of any field or the reply ranker; the
|
|
306
|
+
// owning form then continues, a sub-form's owner included.
|
|
307
|
+
SET('ci_no_owner', 'ci_no_owner', "formNoOwner(cargo, form, ci_spec, cargo.choiceText)"),
|
|
308
|
+
SET('ci_no_flow', 'ci_no_flow', "ci_no_owner ? cargo.forms[ci_no_owner].on_no : ''"),
|
|
309
|
+
SET('ci_no_up', 'ci_no_up', "!!ci_no_owner && ci_no_owner !== form"), // before the call: a shared-scope worker may set `form`
|
|
310
|
+
WHEN('ci_no_run', 'ci_no_flow', FLOW('ci_no_answer', '{{ci_no_flow}}', 'call', { form: '{{ci_no_owner}}' })),
|
|
311
|
+
WHEN('ci_no_owner_resume', 'ci_no_flow && ci_no_up', FLOW('ci_no_resume_owner', 'resume-form', 'reboot', { form: '{{ci_no_owner}}' })),
|
|
312
|
+
WHEN('ci_no_end', 'ci_no_flow', END('ci_no_done')),
|
|
302
313
|
SET('ci_build_chain', 'ci_chain', CHAIN_JS),
|
|
303
314
|
SET('ci_match', 'ci_m', "readFormAnswer(cargo.choiceText, ci_chain, Array.isArray(ci_spec.any_of) ? { form: form, names: ci_names } : { form: form, name: current }, cargo.choice, provides && Array.isArray(questions) ? questions : [])"),
|
|
304
315
|
SIDE('ci_acceptance_trace', "(cargo.formAcceptance = formAcceptanceTrace(cargo.formAcceptance, ci_m), true)"),
|
|
@@ -407,9 +418,9 @@ const rankedChoices = (id, from) => SET(id, 'ranked', `(${from}).concat(${ESCAPE
|
|
|
407
418
|
const RANK_LABEL_JS = "function(c){var said=(Array.isArray(c.phrases) ? c.phrases : []).map(String).filter(function(p){return /[a-zà-ÿ]/i.test(p) && p!==String(c.key);}).slice(0,6);return String(c.label || (said.length ? c.key+' ('+said.join(', ')+')' : c.key));}";
|
|
408
419
|
// The choices of this form's OTHER inputs that the answer could also set (the word reader reads them all: 'LO VENCIDO' at
|
|
409
420
|
// the pay confirmation): the ranker is offered every choice the reader would accept (owner 2026-10-28). A confirmation
|
|
410
|
-
// input is answered only when asked,
|
|
411
|
-
// read as that input's answer.
|
|
412
|
-
const otherChoicesJS = (asked, taken) => `(function(){var seen=(${taken}).map(function(c){return String(c.key);}).concat(['yes','no']);return (chain || []).filter(function(i){return i.form===form && i.name!==${asked} && !i.confirmation && i.profile && i.profile.kind==='choice' && Array.isArray(i.profile.choices);}).reduce(function(all,i){return all.concat(i.profile.choices.filter(function(c){var k=String(c.key);if(seen.indexOf(k)>=0)return false;seen.push(k);return true;}).map(function(c){return {key:String(c.key),label:(${RANK_LABEL_JS})(c),owner:{form:i.form,name:i.name}};}));},[]);})()`;
|
|
421
|
+
// input is answered only when asked, a current_only choice only at its own question (as the reader reads it), and a key
|
|
422
|
+
// already offered is not offered twice. Each carries its input, so a pick is read as that input's answer.
|
|
423
|
+
const otherChoicesJS = (asked, taken) => `(function(){var seen=(${taken}).map(function(c){return String(c.key);}).concat(['yes','no']);return (chain || []).filter(function(i){return i.form===form && i.name!==${asked} && !i.confirmation && i.profile && i.profile.kind==='choice' && Array.isArray(i.profile.choices);}).reduce(function(all,i){return all.concat(i.profile.choices.filter(function(c){var k=String(c.key);if(c.current_only===true||seen.indexOf(k)>=0)return false;seen.push(k);return true;}).map(function(c){return {key:String(c.key),label:(${RANK_LABEL_JS})(c),owner:{form:i.form,name:i.name}};}));},[]);})()`;
|
|
413
424
|
const escapeRoutes = (id) => [WHEN(`${id}_agent`, "reply && reply.key === '__agent'", FLOW(`${id}_agent_flow`, 'live-agent-requested', 'reboot'))];
|
|
414
425
|
const rankedDispatch = (id, reason) => ({ ...yieldDispatch(id, 'form', `'${reason}'`), onResponse: 'continue', responseContext: 'Current question: {{asked}}', responseChoices: 'ranked', responseVariable: 'reply' });
|
|
415
426
|
const readFormReply = {
|
|
@@ -502,8 +513,8 @@ const acceptedEffects = {
|
|
|
502
513
|
WHEN('ie_none', '!ca_h', END('ie_end')),
|
|
503
514
|
SET('ie_rest', 'ca_rest', 'readings.slice(1)'),
|
|
504
515
|
WHEN('ie_reject', "ca_h.outcome === 'rejected'", SIDE('ie_attempt', "(cargo.forms[ca_h.form].attempts[ca_h.asked || ca_h.name] = Number(cargo.forms[ca_h.form].attempts[ca_h.asked || ca_h.name] || 0) + 1, cargo.forms[ca_h.form].feedback=cargo.forms[ca_h.form].feedback || {}, cargo.forms[ca_h.form].feedback[ca_h.asked || ca_h.name]='invalid', true)")),
|
|
505
|
-
SET('ie_say_en', 'ca_ack', "ca_h.outcome === 'rejected' && ca_h.quiet ? '' : ca_h.outcome === 'rejected' ? (ca_h.message || ('That ' + (ca_h.label || ca_h.name) + \" doesn't look right.\")) : ca_h.silent === true || ca_h.fromPending ? '' : ca_h.outcome === 'known' && !ca_h.current ? ('Thank you, I already have your ' + ca_h.label + '.') : ca_h.outcome === 'changed' ? ('Okay, I noted your ' + ca_h.label + '.') : (ca_h.outcome === 'taken' || ca_h.outcome === 'pending') && ca_h.origin !== 'activation' ? ('Got it, your ' + ca_h.label + '.') : ''"),
|
|
506
|
-
SET('ie_say_es', 'ca_ack_es', "ca_h.outcome === 'rejected' && ca_h.quiet ? '' : ca_h.outcome === 'rejected' ? (ca_h.message_es || ca_h.message || ('Ese ' + (ca_h.label_es || ca_h.label || ca_h.name) + ' no parece correcto.')) : ca_h.silent === true || ca_h.fromPending ? '' : ca_h.outcome === 'known' && !ca_h.current ? ('Gracias, ya tengo su ' + ca_h.label_es + '.') : ca_h.outcome === 'changed' ? ('Listo, anoté su ' + ca_h.label_es + '.') : (ca_h.outcome === 'taken' || ca_h.outcome === 'pending') && ca_h.origin !== 'activation' ? ('Entendido, su ' + ca_h.label_es + '.') : ''"),
|
|
516
|
+
SET('ie_say_en', 'ca_ack', "ca_h.outcome === 'rejected' && ca_h.quiet ? '' : ca_h.outcome === 'rejected' ? (ca_h.message || ('That ' + (ca_h.label || ca_h.name) + \" doesn't look right.\")) : ca_h.silent === true || ca_h.fromPending ? '' : ca_h.outcome === 'known' && !ca_h.current ? ('Thank you, I already have your ' + ca_h.label + '.') : ca_h.outcome === 'changed' && ca_h.origin !== 'activation' ? ('Okay, I noted your ' + ca_h.label + '.') : (ca_h.outcome === 'taken' || ca_h.outcome === 'pending') && ca_h.origin !== 'activation' ? ('Got it, your ' + ca_h.label + '.') : ''"),
|
|
517
|
+
SET('ie_say_es', 'ca_ack_es', "ca_h.outcome === 'rejected' && ca_h.quiet ? '' : ca_h.outcome === 'rejected' ? (ca_h.message_es || ca_h.message || ('Ese ' + (ca_h.label_es || ca_h.label || ca_h.name) + ' no parece correcto.')) : ca_h.silent === true || ca_h.fromPending ? '' : ca_h.outcome === 'known' && !ca_h.current ? ('Gracias, ya tengo su ' + ca_h.label_es + '.') : ca_h.outcome === 'changed' && ca_h.origin !== 'activation' ? ('Listo, anoté su ' + ca_h.label_es + '.') : (ca_h.outcome === 'taken' || ca_h.outcome === 'pending') && ca_h.origin !== 'activation' ? ('Entendido, su ' + ca_h.label_es + '.') : ''"),
|
|
507
518
|
WHEN('ie_say', 'ca_ack', SAY('ie_ack', '{{ca_ack}}', '{{ca_ack_es}}')),
|
|
508
519
|
WHEN('ie_callback', "ca_h.after && ca_h.outcome !== 'known' && ca_h.outcome !== 'rejected' && ca_h.outcome !== 'pending'", FLOW('ie_after', '{{ca_h.after}}', 'call', { form: '{{ca_h.form}}', input: '{{ca_h.name}}', value: '{{ca_h.value}}' })),
|
|
509
520
|
WHEN('ie_more', 'ca_rest.length > 0', FLOW('ie_next', 'input-accepted-effects', 'replace', { readings: '{{ca_rest}}' })),
|
|
@@ -521,8 +532,8 @@ const disambiguate = {
|
|
|
521
532
|
SET('di_build', 'di_choices', "readings.map(function (r, i) { var own = String(r.label + ' ' + r.label_es).toLowerCase().split(/\\s+/).filter(function (w) { return w.length > 3 && !readings.some(function (o, j) { return j !== i && String(o.label + ' ' + o.label_es).toLowerCase().split(/\\s+/).indexOf(w) > -1; }); }); return { key: 'r' + i, phrases: [di_keys[i], r.label, r.label_es].concat(own) }; })"),
|
|
522
533
|
SET('di_said', 'di_value', "readings[0].pii ? '' : String(readings[0].value)"),
|
|
523
534
|
// Options of ONE input (a phrase two choices share) are asked by name: "Do you mean A or B?". The value is a choice key, never words to say.
|
|
524
|
-
SET('di_question', 'di_q', "(readings.every(function (r) { return r.option === true; }) ? 'Do you mean ' + readings.map(function (r) { return r.label; }).join(' or ') : (di_value ? 'Is ' + di_value + ' your ' : 'Is that your ') + readings.map(function (r) { return r.label; }).join(' or your ')) + '? ' + readings.map(function (r, i) { return (cargo.voice ? 'Press ' + di_keys[i] + ' or say ' : '
|
|
525
|
-
SET('di_question_es', 'di_q_es', "(readings.every(function (r) { return r.option === true; }) ? '¿Se refiere a ' + readings.map(function (r) { return r.label_es; }).join(' o a ') : (di_value ? '¿' + di_value + ' es su ' : '¿Eso es su ') + readings.map(function (r) { return r.label_es; }).join(' o su ')) + '? ' + readings.map(function (r, i) { return (cargo.voice ? 'Presione ' + di_keys[i] + ' o diga ' : '
|
|
535
|
+
SET('di_question', 'di_q', "(readings.every(function (r) { return r.option === true; }) ? 'Do you mean ' + readings.map(function (r) { return r.label; }).join(' or ') : (di_value ? 'Is ' + di_value + ' your ' : 'Is that your ') + readings.map(function (r) { return r.label; }).join(' or your ')) + '? ' + readings.map(function (r, i) { return (cargo.voice ? 'Press ' + di_keys[i] + ' or say ' : (i ? String(cargo.verb || 'type') : String(cargo.verb || 'type').charAt(0).toUpperCase() + String(cargo.verb || 'type').slice(1)) + ' ') + r.label.toUpperCase(); }).join(', ') + '.'"),
|
|
536
|
+
SET('di_question_es', 'di_q_es', "(readings.every(function (r) { return r.option === true; }) ? '¿Se refiere a ' + readings.map(function (r) { return r.label_es; }).join(' o a ') : (di_value ? '¿' + di_value + ' es su ' : '¿Eso es su ') + readings.map(function (r) { return r.label_es; }).join(' o su ')) + '? ' + readings.map(function (r, i) { return (cargo.voice ? 'Presione ' + di_keys[i] + ' o diga ' : (i ? String(cargo.verb_es || 'escriba') : String(cargo.verb_es || 'escriba').charAt(0).toUpperCase() + String(cargo.verb_es || 'escriba').slice(1)) + ' ') + r.label_es.toUpperCase(); }).join(', ') + '.'"),
|
|
526
537
|
SET('di_owner', 'di_form', 'readings[0].form'),
|
|
527
538
|
SET('di_must_complete', 'di_must', 'readings.some(function(r){return formMustComplete(cargo,r.form,r.name);})'),
|
|
528
539
|
SET('di_counter', 'di_input', "'__ambiguity:'+readings.map(function(r){return r.form+':'+r.name;}).sort().join('|')"),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"identity-flows.d.ts","sourceRoot":"","sources":["../../src/forms/identity-flows.js"],"names":[],"mappings":"AA2BA,+GAA+G;AAC/G,oFAOC;AAQD,wFAAwF;AACxF,iDAGC;AACD,6CAMC;AACD,qHAQC;AAMD,4EAKC;AAoFD,4DAKC;
|
|
1
|
+
{"version":3,"file":"identity-flows.d.ts","sourceRoot":"","sources":["../../src/forms/identity-flows.js"],"names":[],"mappings":"AA2BA,+GAA+G;AAC/G,oFAOC;AAQD,wFAAwF;AACxF,iDAGC;AACD,6CAMC;AACD,qHAQC;AAMD,4EAKC;AAoFD,4DAKC;AAmHD,8EAGC;AA4BD,4FAA4F;AAC5F;;;;EAOC;AAQD,0BAAmI"}
|
|
@@ -280,8 +280,11 @@ for (const flow of [auth, validateCode]) {
|
|
|
280
280
|
flow.steps.unshift(...otpMenuMessages.map(([name, value]) => set(name, name, value.includes('{{') ? value : JSON.stringify(value))));
|
|
281
281
|
}
|
|
282
282
|
const retryParams = [param('error_message', 'Failure sentence'), param('error_message_es', 'Spanish failure sentence'), param('retry_flow', 'Retry destination'), param('cancel_flow', 'Exit destination'), param('capture_patterns', 'Declared contact capture readers', 'array'), param('retry_call_type', 'reboot or replace'), param('allow_code_entry', 'Accept the active six-digit challenge', 'boolean'), param('question', 'Retry question'), param('question_es', 'Spanish retry question'), param('caller_outcome', 'Unresolved outcome'), param('caller_reason', 'Unresolved reason'), param('parent_form', 'Owning task FORM instance'), param('contact_options', 'Contact field protocol declaration', 'object')];
|
|
283
|
-
const retry = worker('generic-retry-with-options', 'GenericRetryWithOptions', 'Collect a retry, alternate contact, code or exit through the shared FORM adapter.', { scope: 'isolated', parameters: retryParams, variables: vars({ error_message: '', error_message_es: '', retry_flow: 'authenticate-user', cancel_flow: 'contact-support', capture_patterns: [], retry_call_type: 'reboot', allow_code_entry: false, question: '', question_es: '', caller_outcome: '', caller_reason: '', parent_form: '', contact_options: null, user_choice: '', smart_capture_result: null, normalized_otp_code: '', retry_question: '', retry_question_es: '', retry_yes_words: ['phone', 'email', 'cell', 'mobile', 'teléfono', 'telefono', 'celular', 'móvil', 'movil', 'correo electrónico', 'correo', 'try again', 'try it again', 'retry', 'intentar', 'intentarlo', 'intentar de nuevo', 'intentar nuevamente', 'intentarlo de nuevo', 'intentarlo otra vez', 'de nuevo', 'nuevamente', 'otra vez', 'reintentar', 'volver a intentar', 'volver a intentarlo'],
|
|
283
|
+
const retry = worker('generic-retry-with-options', 'GenericRetryWithOptions', 'Collect a retry, alternate contact, code or exit through the shared FORM adapter.', { scope: 'isolated', parameters: retryParams, variables: vars({ error_message: '', error_message_es: '', retry_flow: 'authenticate-user', cancel_flow: 'contact-support', capture_patterns: [], retry_call_type: 'reboot', allow_code_entry: false, question: '', question_es: '', caller_outcome: '', caller_reason: '', parent_form: '', contact_options: null, user_choice: '', smart_capture_result: null, normalized_otp_code: '', retry_question: '', retry_question_es: '', retry_yes_words: ['phone', 'email', 'cell', 'mobile', 'teléfono', 'telefono', 'celular', 'móvil', 'movil', 'correo electrónico', 'correo', 'try again', 'try it again', 'retry', 'intentar', 'intentarlo', 'intentar de nuevo', 'intentar nuevamente', 'intentarlo de nuevo', 'intentarlo otra vez', 'de nuevo', 'nuevamente', 'otra vez', 'reintentar', 'volver a intentar', 'volver a intentarlo'], retry_otp_profiles: OTP_PROFILES, retry_profiles_base: [{ name: 'phone', label: 'phone number', label_es: 'número de teléfono', profile: { kind: 'phone' } }, { name: 'email_address', label: 'email address', label_es: 'correo electrónico', profile: { kind: 'email' } }], retry_profiles: [] }), contract: { role: 'collector', summary: 'Common bounded service retry menu' }, steps: [
|
|
284
284
|
set('retry_parent', 'parent_form', "parent_form || cargo.authOptions && cargo.authOptions.parent_form || cargo.contactOptions && cargo.contactOptions.parent_form || ''"),
|
|
285
|
+
// The menu answers what its capture declares: an account number in the tenant's pattern (get-cell-or-email's
|
|
286
|
+
// account_pattern, carried in cargo.authCapture) is the menu's own answer, never the parent's, so it reaches the lookup.
|
|
287
|
+
set('retry_profiles', 'retry_profiles', "retry_profiles_base.concat((Array.isArray(capture_patterns) ? capture_patterns : []).filter(function (c) {return c && c.variable === 'acct_number' && c.regex;}).slice(0,1).map(function (c) {return {name:'account',label:'account number',label_es:'número de cuenta',profile:{kind:'identifier',pattern:c.regex}};}),retry_otp_profiles)"),
|
|
285
288
|
set('retry_question', 'retry_question', "error_message+' '+(question || 'Would you like to try again?')+' To retry '+(cargo.voice?'Press 1 or ':'')+cargo.verb+' YES. '+(callerTextNumber(cargo)?'To switch to text press 9 or '+cargo.verb+' TEXT. ':'')+'To abort '+(cargo.voice?'press the asterisk key or ':'')+cargo.verb+' EXIT.'"),
|
|
286
289
|
set('retry_question_es', 'retry_question_es', "error_message_es+' '+(question_es || '¿Le gustaría intentar nuevamente?')+' Para reintentar '+(cargo.voice?'Presione 1 o ':'')+cargo.verb_es+' SÍ. '+(callerTextNumber(cargo)?'Para cambiar a texto presione 9 o '+cargo.verb_es+' TEXTO. ':'')+'Para salir '+(cargo.voice?'presione la tecla de asterisco o ':'')+cargo.verb_es+' SALIR.'"),
|
|
287
290
|
call('retry_collect', 'collect-service-answer', { profiles: '{{retry_profiles}}', question: '{{retry_question}}', question_es: '{{retry_question_es}}', keypad: { min: 1, max: 1 }, parent_form: '{{parent_form}}' }),
|
package/dist/helpers/answer.d.ts
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
* YES, AGENT and TEXT never match a negated answer ("not sure", "I don't want an agent"); 0 is AGENT, * is EXIT.
|
|
4
4
|
*/
|
|
5
5
|
export function isAnswer(input: any, kind: any, extra?: any[]): boolean;
|
|
6
|
+
/** Whether the WHOLE answer is a NO ("No.", "no gracias", "nope, thanks"), never a sentence that starts with one. */
|
|
7
|
+
export function isWholeNo(input: any): boolean;
|
|
6
8
|
/**
|
|
7
9
|
* Whether the WHOLE answer is an exit word ("exit", "Cancelar.", "salir por favor", "*"), never a sentence that contains
|
|
8
10
|
* one: "Cancelar el pedido" asks for something; it does not withdraw (prod 2026-10-26, at a verification-code prompt).
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"answer.d.ts","sourceRoot":"","sources":["../../src/helpers/answer.js"],"names":[],"mappings":"AAsNA;;;GAGG;AACH,wEAkBC;AAED;;;;GAIG;AACH,mDAIC;AAED,+HAA+H;AAC/H,oDAIC;AAcD,2GAA2G;AAC3G,uDAGC;AAOD;;;;;GAKG;AACH,+DAUC;AAKD;;;;GAIG;AACH,oDAEC;AAqBD;;;;;;GAMG;AACH,mDAAmF;AAEnF,kGAAkG;AAClG;;;EAoCC;AAUD;;;;;;;;;GASG;AACH,6EA4GC;;;;;;;;
|
|
1
|
+
{"version":3,"file":"answer.d.ts","sourceRoot":"","sources":["../../src/helpers/answer.js"],"names":[],"mappings":"AAsNA;;;GAGG;AACH,wEAkBC;AAED,qHAAqH;AACrH,+CAGC;AAED;;;;GAIG;AACH,mDAIC;AAED,+HAA+H;AAC/H,oDAIC;AAcD,2GAA2G;AAC3G,uDAGC;AAOD;;;;;GAKG;AACH,+DAUC;AAKD;;;;GAIG;AACH,oDAEC;AAqBD;;;;;;GAMG;AACH,mDAAmF;AAEnF,kGAAkG;AAClG;;;EAoCC;AAUD;;;;;;;;;GASG;AACH,6EA4GC;;;;;;;;AA9cD,qCAKwE"}
|
package/dist/helpers/answer.js
CHANGED
|
@@ -250,6 +250,11 @@ export function isAnswer(input, kind, extra = []) {
|
|
|
250
250
|
return matchesChoice(b, words);
|
|
251
251
|
}
|
|
252
252
|
}
|
|
253
|
+
/** Whether the WHOLE answer is a NO ("No.", "no gracias", "nope, thanks"), never a sentence that starts with one. */
|
|
254
|
+
export function isWholeNo(input) {
|
|
255
|
+
const b = normalize(input);
|
|
256
|
+
return !!b && onlyAffirmative(b, ANSWER_WORDS.no);
|
|
257
|
+
}
|
|
253
258
|
/**
|
|
254
259
|
* Whether the WHOLE answer is an exit word ("exit", "Cancelar.", "salir por favor", "*"), never a sentence that contains
|
|
255
260
|
* one: "Cancelar el pedido" asks for something; it does not withdraw (prod 2026-10-26, at a verification-code prompt).
|
|
@@ -54,4 +54,7 @@ export function cancelFormTask(cargo: any, id: any): {
|
|
|
54
54
|
flow: any;
|
|
55
55
|
};
|
|
56
56
|
};
|
|
57
|
+
/** The form that answers a whole NO said at this question: the nearest form up the chain (this one, or a sub-form's owner)
|
|
58
|
+
* that declares `on_no`, when the question is not a yes/no or confirmation and does not read the NO itself. '' if none. */
|
|
59
|
+
export function formNoOwner(cargo: any, id: any, spec: any, answer: any): any;
|
|
57
60
|
//# sourceMappingURL=form-conversation.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"form-conversation.d.ts","sourceRoot":"","sources":["../../src/helpers/form-conversation.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"form-conversation.d.ts","sourceRoot":"","sources":["../../src/helpers/form-conversation.js"],"names":[],"mappings":"AAKA,0EAGC;AACD,oEAGC;AACD;;qGAEqG;AACrG,2EAWC;AACD;;;GAGG;AACH,+GAQC;AACD,8EAAwI;AAExI,yGAAyG;AACzG;;;;;;;;;;;;;;;;;;;;EAUC;AAED,sGAAsG;AACtG;;;;;;;;;;;;;;;;;;;;EAeC;AAED;2HAC2H;AAC3H,8EAOC"}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
/** Shared FORM retry, contextual yield and recovery policy; all state is conversation-owned. */
|
|
2
2
|
import { formIncompleteOutcome, closeFormInstance } from './form-state.js';
|
|
3
|
+
import { isWholeNo } from './answer.js';
|
|
4
|
+
import { readInputProfile } from './match-inputs.js';
|
|
3
5
|
export function clearFormMisses(cargo, id, input) {
|
|
4
6
|
const state = cargo.forms?.[id];
|
|
5
7
|
if (!state)
|
|
@@ -110,3 +112,19 @@ export function cancelFormTask(cargo, id) {
|
|
|
110
112
|
closeFormInstance(cargo, owner, false);
|
|
111
113
|
return { outcome: 'resolved', reason: 'request_cancelled', recovery: { version: 1, mode: 'none', reason: 'request_cancelled' } };
|
|
112
114
|
}
|
|
115
|
+
/** The form that answers a whole NO said at this question: the nearest form up the chain (this one, or a sub-form's owner)
|
|
116
|
+
* that declares `on_no`, when the question is not a yes/no or confirmation and does not read the NO itself. '' if none. */
|
|
117
|
+
export function formNoOwner(cargo, id, spec, answer) {
|
|
118
|
+
const raw = String(answer ?? '').replace(/\\(['"\\])/g, '$1').trim();
|
|
119
|
+
if (!raw || !isWholeNo(raw) || !spec || spec.confirmation || spec.proposal)
|
|
120
|
+
return '';
|
|
121
|
+
if ([spec, ...(Array.isArray(spec.any_of) ? spec.any_of : [])].some(s => s && s.profile && readInputProfile(s.profile, raw, true)))
|
|
122
|
+
return ''; // the question offers NO itself
|
|
123
|
+
const seen = new Set();
|
|
124
|
+
for (let f = id; f && cargo.forms?.[f] && !seen.has(f); f = cargo.forms[f].parent) {
|
|
125
|
+
seen.add(f);
|
|
126
|
+
if (cargo.forms[f].on_no)
|
|
127
|
+
return f;
|
|
128
|
+
}
|
|
129
|
+
return '';
|
|
130
|
+
}
|
|
@@ -18,7 +18,7 @@ export function settleFormActivations(batch?: {}, cargo?: null): {
|
|
|
18
18
|
pending: any[];
|
|
19
19
|
};
|
|
20
20
|
/** Explicit ownership: unrelated tasks and sibling inputs never share an instance by definition id. */
|
|
21
|
-
export function openFormInstance(cargo: any, definition: any, specs: any, { parent, input, resume, task, providers, questions, context, resume_on_parent_change, incomplete_outcome, on_exhausted, must_complete }?: {
|
|
21
|
+
export function openFormInstance(cargo: any, definition: any, specs: any, { parent, input, resume, task, providers, questions, context, resume_on_parent_change, incomplete_outcome, on_exhausted, on_no, must_complete }?: {
|
|
22
22
|
parent?: null | undefined;
|
|
23
23
|
input?: string | undefined;
|
|
24
24
|
resume?: string | undefined;
|
|
@@ -29,6 +29,7 @@ export function openFormInstance(cargo: any, definition: any, specs: any, { pare
|
|
|
29
29
|
resume_on_parent_change?: undefined;
|
|
30
30
|
incomplete_outcome?: undefined;
|
|
31
31
|
on_exhausted?: undefined;
|
|
32
|
+
on_no?: undefined;
|
|
32
33
|
must_complete?: undefined;
|
|
33
34
|
}): any;
|
|
34
35
|
/** An incomplete optional follow-up may preserve an explicitly completed task outcome. */
|
|
@@ -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;AA0CD,2FAEC;AACD,8FAcC;AACD,2DAOC;AACD,uEA6CC;AAED,kGAAkG;AAClG,2GAOC;AACD,0HAA0H;AAC1H,uEAwBC;AACD,8HAA8H;AAC9H;;;;;EAWC;AACD,uGAAuG;AACvG
|
|
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;;;;;;;;;;;;;QAoCC;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;AArpBM,8CAAmE"}
|
|
@@ -293,7 +293,7 @@ export function settleFormActivations(batch = {}, cargo = null) {
|
|
|
293
293
|
rejected: rejected.map(h => h.activation_inferred ? { ...h, ...h.activation_entry, activation_inferred: false } : h), pending: pending.concat(accepted.filter(h => held.has(h.activation_group))).filter(h => !discarded.has(h.activation_group)).map(h => held.has(h.activation_group) ? { ...h, ok: undefined, outcome: 'pending', silent: true } : h) };
|
|
294
294
|
}
|
|
295
295
|
/** Explicit ownership: unrelated tasks and sibling inputs never share an instance by definition id. */
|
|
296
|
-
export function openFormInstance(cargo, definition, specs, { parent = null, input = '', resume = '', task = '', providers = null, questions = null, context = false, resume_on_parent_change = undefined, incomplete_outcome = undefined, on_exhausted = undefined, must_complete = undefined } = {}) {
|
|
296
|
+
export function openFormInstance(cargo, definition, specs, { parent = null, input = '', resume = '', task = '', providers = null, questions = null, context = false, resume_on_parent_change = undefined, incomplete_outcome = undefined, on_exhausted = undefined, on_no = undefined, must_complete = undefined } = {}) {
|
|
297
297
|
const incomplete = validateFormIncompleteOutcome(incomplete_outcome);
|
|
298
298
|
validateFormExhaustion(on_exhausted);
|
|
299
299
|
cargo.forms ||= {};
|
|
@@ -309,6 +309,8 @@ export function openFormInstance(cargo, definition, specs, { parent = null, inpu
|
|
|
309
309
|
existing.incomplete_outcome = incomplete;
|
|
310
310
|
if (on_exhausted !== undefined)
|
|
311
311
|
existing.on_exhausted = on_exhausted;
|
|
312
|
+
if (on_no !== undefined)
|
|
313
|
+
existing.on_no = on_no || '';
|
|
312
314
|
if (must_complete !== undefined)
|
|
313
315
|
existing.must_complete = must_complete === true;
|
|
314
316
|
return resume;
|
|
@@ -327,6 +329,8 @@ export function openFormInstance(cargo, definition, specs, { parent = null, inpu
|
|
|
327
329
|
cargo.forms[child].incomplete_outcome = incomplete;
|
|
328
330
|
if (on_exhausted !== undefined)
|
|
329
331
|
cargo.forms[child].on_exhausted = on_exhausted;
|
|
332
|
+
if (on_no !== undefined)
|
|
333
|
+
cargo.forms[child].on_no = on_no || '';
|
|
330
334
|
if (must_complete !== undefined)
|
|
331
335
|
cargo.forms[child].must_complete = must_complete === true;
|
|
332
336
|
return child;
|
|
@@ -335,7 +339,7 @@ export function openFormInstance(cargo, definition, specs, { parent = null, inpu
|
|
|
335
339
|
cargo.formSequence = Number(cargo.formSequence || 0) + 1;
|
|
336
340
|
const id = `${definition}:${cargo.formSequence}`;
|
|
337
341
|
cargo.forms[id] = { id, definition, task: owner?.task || task || id, parent, result_input: input,
|
|
338
|
-
on_exhausted: on_exhausted || '', must_complete: must_complete === true, incomplete_outcome: incomplete, context_only: !!context, resume_on_parent_change: resume_on_parent_change === true, parent_revision: owner?.revision ?? null, specs, values: {}, accepted: {}, errors: {}, pending: {}, seeds: {}, attempts: {}, misses: {}, feedback: {}, prepared: {}, data: {}, arguments: {}, contexts: {}, children: {}, childResults: {}, revision: 0,
|
|
342
|
+
on_exhausted: on_exhausted || '', on_no: on_no || '', must_complete: must_complete === true, incomplete_outcome: incomplete, context_only: !!context, resume_on_parent_change: resume_on_parent_change === true, parent_revision: owner?.revision ?? null, specs, values: {}, accepted: {}, errors: {}, pending: {}, seeds: {}, attempts: {}, misses: {}, feedback: {}, prepared: {}, data: {}, arguments: {}, contexts: {}, children: {}, childResults: {}, revision: 0,
|
|
339
343
|
providers: providers || owner?.providers || null, questions: questions?.length ? questions : owner?.questions || [], status: 'collecting' };
|
|
340
344
|
if (owner) {
|
|
341
345
|
if (context) {
|
package/dist/helpers/index.d.ts
CHANGED
|
@@ -18,6 +18,7 @@ export namespace STANDARD_FUNCTIONS {
|
|
|
18
18
|
export { validateFormProposalDescriptor };
|
|
19
19
|
export { formProposalDescriptor };
|
|
20
20
|
export { clearFormMisses };
|
|
21
|
+
export { formNoOwner };
|
|
21
22
|
export { missFormInput };
|
|
22
23
|
export { formMustComplete };
|
|
23
24
|
export { formInputPrompt };
|
|
@@ -136,6 +137,7 @@ import { validateFormProposalPolicy } from './proposal-contract.js';
|
|
|
136
137
|
import { validateFormProposalDescriptor } from './proposal-contract.js';
|
|
137
138
|
import { formProposalDescriptor } from './proposal-contract.js';
|
|
138
139
|
import { clearFormMisses } from './form-conversation.js';
|
|
140
|
+
import { formNoOwner } from './form-conversation.js';
|
|
139
141
|
import { missFormInput } from './form-conversation.js';
|
|
140
142
|
import { formMustComplete } from './form-conversation.js';
|
|
141
143
|
import { formInputPrompt } from './form-conversation.js';
|
|
@@ -240,5 +242,5 @@ import { validatePhone } from './validators.js';
|
|
|
240
242
|
import { validateEmail } from './validators.js';
|
|
241
243
|
import { urlToSpeech } from './url-speech.js';
|
|
242
244
|
import { textWithUrlToSpeech } from './url-speech.js';
|
|
243
|
-
export { continuationSafetyProtocol, cancelFormTask, contextualResponseProtocol, validateFormResponse, hasFormResponse, formProposalPolicy, stageFormProposal, nextFormProposal, formProposalReadings, bindFormProposal, decideFormProposal, commitFormProposals, settleFormProposalValidation, formProposalProtocol, validateFormProposalPolicy, validateFormProposalDescriptor, formProposalDescriptor, clearFormMisses, missFormInput, formMustComplete, formInputPrompt, consumeFormFeedback, formConversationOutcome, readMoneyAmount, isMoneyAmount, moneyIntentAllows, isPastPaymentStatement, isPartialMoneyAnswer, validateMoneyOptions, takeChoiceOffer, matchesChoice, ANSWER_WORDS, CONFIRM_WORDS, isAnswer, isAgentRequest, isExitRequest, isOtherRequest, isSpelledFragment, isAmountAnswer, amountInAnswer, pickChoice, readEmail, readPhone, readInternationalPhone, readIdentifier, last4, maskEmail, callerTextNumber, textDestination, callerIsInternational, normalizeAndFindCapture, anythingElse, goodbye, sayName, dateToWords, amountToSpeech, validateCardNumber, validateExpiration, consumeActionReceipt, authorizeFormActionRetry, formSignature, selectFormAction, confirmFormRevision, prepareFormAction, actionConfirmationValid, beginFormAction, finishFormAction, actionGateVerified, sourceNeedsLoad, beginSourceRefresh, completeSourceRefresh, pickProviderQuestion, matchInputs, readFormAnswer, readInputProfile, formAcceptanceTrace, resolveFormInputs, formInputApplies, formInputFilled, nextFormInput, formInputChain, openFormInstance, validateFormIncompleteOutcome, formIncompleteOutcome, validateFormExhaustion, exhaustFormInput, formInitialReadings, orderFormReadings, proposedFormValues, applyFormValidation, formReadingDependencies, exclusiveFormReadings, enrichFormMatches, expandFormActivations, settleFormActivations, rememberFormArguments, resetFormInputs, hasFormValue, formPath, formValuesEqual, formReadingReady, formReadingRelevant, formPreparation, queueFormReadings, pendingFormReadings, formWorkLimit, answerResetsFormReplays, rejectFormReadings, invalidateFormInputs, acceptFormReadings, formInstanceApplies, formParentResume, closeFormInstance, checkOtp, maskDestination, validateOTP, OTP_EXPIRY_MS, validateDigits, validatePhone, validateEmail, urlToSpeech, textWithUrlToSpeech };
|
|
245
|
+
export { continuationSafetyProtocol, cancelFormTask, contextualResponseProtocol, validateFormResponse, hasFormResponse, formProposalPolicy, stageFormProposal, nextFormProposal, formProposalReadings, bindFormProposal, decideFormProposal, commitFormProposals, settleFormProposalValidation, formProposalProtocol, validateFormProposalPolicy, validateFormProposalDescriptor, formProposalDescriptor, clearFormMisses, formNoOwner, missFormInput, formMustComplete, formInputPrompt, consumeFormFeedback, formConversationOutcome, readMoneyAmount, isMoneyAmount, moneyIntentAllows, isPastPaymentStatement, isPartialMoneyAnswer, validateMoneyOptions, takeChoiceOffer, matchesChoice, ANSWER_WORDS, CONFIRM_WORDS, isAnswer, isAgentRequest, isExitRequest, isOtherRequest, isSpelledFragment, isAmountAnswer, amountInAnswer, pickChoice, readEmail, readPhone, readInternationalPhone, readIdentifier, last4, maskEmail, callerTextNumber, textDestination, callerIsInternational, normalizeAndFindCapture, anythingElse, goodbye, sayName, dateToWords, amountToSpeech, validateCardNumber, validateExpiration, consumeActionReceipt, authorizeFormActionRetry, formSignature, selectFormAction, confirmFormRevision, prepareFormAction, actionConfirmationValid, beginFormAction, finishFormAction, actionGateVerified, sourceNeedsLoad, beginSourceRefresh, completeSourceRefresh, pickProviderQuestion, matchInputs, readFormAnswer, readInputProfile, formAcceptanceTrace, resolveFormInputs, formInputApplies, formInputFilled, nextFormInput, formInputChain, openFormInstance, validateFormIncompleteOutcome, formIncompleteOutcome, validateFormExhaustion, exhaustFormInput, formInitialReadings, orderFormReadings, proposedFormValues, applyFormValidation, formReadingDependencies, exclusiveFormReadings, enrichFormMatches, expandFormActivations, settleFormActivations, rememberFormArguments, resetFormInputs, hasFormValue, formPath, formValuesEqual, formReadingReady, formReadingRelevant, formPreparation, queueFormReadings, pendingFormReadings, formWorkLimit, answerResetsFormReplays, rejectFormReadings, invalidateFormInputs, acceptFormReadings, formInstanceApplies, formParentResume, closeFormInstance, checkOtp, maskDestination, validateOTP, OTP_EXPIRY_MS, validateDigits, validatePhone, validateEmail, urlToSpeech, textWithUrlToSpeech };
|
|
244
246
|
//# 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;+BAI0E,wBAAwB;2CAH9F,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;gCACQ,wBAAwB;4BAAxB,wBAAwB;8BAAxB,wBAAwB;iCAAxB,wBAAwB;gCAAxB,wBAAwB;oCAAxB,wBAAwB;wCAAxB,wBAAwB;gCAmB7C,YAAY;8BAAZ,YAAY;kCAAZ,YAAY;uCAAZ,YAAY;qCAAZ,YAAY;qCAAZ,YAAY;gCAlB3G,mBAAmB;8BAiBrB,qBAAqB;6BAEiH,aAAa;8BAAb,aAAa;yBAAb,aAAa;+BAAb,aAAa;8BAAb,aAAa;+BAAb,aAAa;kCAAb,aAAa;+BAAb,aAAa;+BAAb,aAAa;2BAAb,aAAa;0BACxB,cAAc;0BAAd,cAAc;uCAAd,cAAc;+BAAd,cAAc;sBAAd,cAAc;0BAAd,cAAc;iCAAd,cAAc;gCAAd,cAAc;sCAAd,cAAc;wCAC/H,cAAc;6BACP,cAAc;wBAAd,cAAc;wBAAd,cAAc;4BACjC,iBAAiB;+BACd,uBAAuB;mCACC,kBAAkB;mCAAlB,kBAAkB;qCAxB+I,mBAAmB;yCAAnB,mBAAmB;8BAAnB,mBAAmB;iCAAnB,mBAAmB;oCAAnB,mBAAmB;kCAAnB,mBAAmB;wCAAnB,mBAAmB;gCAAnB,mBAAmB;iCAAnB,mBAAmB;mCAAnB,mBAAmB;gCAe1I,mBAAmB;mCAAnB,mBAAmB;sCAAnB,mBAAmB;qCAAnB,mBAAmB;4BAUjC,mBAAmB;+BAAnB,mBAAmB;iCAAnB,mBAAmB;oCAAnB,mBAAmB;kCACkoB,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;yBACnrB,gBAAgB;gCAAhB,gBAAgB;4BAAhB,gBAAgB;8BAAhB,gBAAgB;+BACzB,iBAAiB;8BAAjB,iBAAiB;8BAAjB,iBAAiB;4BAC7B,iBAAiB;oCAAjB,iBAAiB"}
|
package/dist/helpers/index.js
CHANGED
|
@@ -3,7 +3,7 @@ import { continuationSafetyProtocol } from '../forms/continuation-safety.js';
|
|
|
3
3
|
import { contextualResponseProtocol, validateFormResponse, hasFormResponse } from './contextual-response.js';
|
|
4
4
|
import { formProposalPolicy, stageFormProposal, nextFormProposal, formProposalReadings, bindFormProposal, decideFormProposal, commitFormProposals, settleFormProposalValidation } from './form-proposals.js';
|
|
5
5
|
import { formProposalProtocol, validateFormProposalPolicy, validateFormProposalDescriptor, formProposalDescriptor } from './proposal-contract.js';
|
|
6
|
-
import { cancelFormTask, clearFormMisses, missFormInput, formMustComplete, formInputPrompt, consumeFormFeedback, formConversationOutcome } from './form-conversation.js';
|
|
6
|
+
import { cancelFormTask, clearFormMisses, formNoOwner, missFormInput, formMustComplete, formInputPrompt, consumeFormFeedback, formConversationOutcome } from './form-conversation.js';
|
|
7
7
|
import { takeChoiceOffer } from './choice-offer.js';
|
|
8
8
|
import { consumeActionReceipt, authorizeFormActionRetry, formSignature, selectFormAction, confirmFormRevision, prepareFormAction, actionConfirmationValid, beginFormAction, finishFormAction, actionGateVerified } from './form-actions.js';
|
|
9
9
|
/**
|
|
@@ -35,12 +35,12 @@ import { resolveFormInputs, formInputApplies, formInputFilled, nextFormInput, fo
|
|
|
35
35
|
import { checkOtp, maskDestination, validateOTP, OTP_EXPIRY_MS } from './otp-check.js';
|
|
36
36
|
import { validateDigits, validatePhone, validateEmail } from './validators.js';
|
|
37
37
|
import { urlToSpeech, textWithUrlToSpeech } from './url-speech.js';
|
|
38
|
-
export { continuationSafetyProtocol, cancelFormTask, contextualResponseProtocol, validateFormResponse, hasFormResponse, formProposalPolicy, stageFormProposal, nextFormProposal, formProposalReadings, bindFormProposal, decideFormProposal, commitFormProposals, settleFormProposalValidation, formProposalProtocol, validateFormProposalPolicy, validateFormProposalDescriptor, formProposalDescriptor, clearFormMisses, missFormInput, formMustComplete, formInputPrompt, consumeFormFeedback, formConversationOutcome, readMoneyAmount, isMoneyAmount, moneyIntentAllows, isPastPaymentStatement, isPartialMoneyAnswer, validateMoneyOptions, takeChoiceOffer, matchesChoice, ANSWER_WORDS, CONFIRM_WORDS, isAnswer, isAgentRequest, isExitRequest, isOtherRequest, isSpelledFragment, isAmountAnswer, amountInAnswer, pickChoice, readEmail, readPhone, readInternationalPhone, readIdentifier, last4, maskEmail, callerTextNumber, textDestination, callerIsInternational, normalizeAndFindCapture, anythingElse, goodbye, sayName, dateToWords, amountToSpeech, validateCardNumber, validateExpiration, consumeActionReceipt, authorizeFormActionRetry, formSignature, selectFormAction, confirmFormRevision, prepareFormAction, actionConfirmationValid, beginFormAction, finishFormAction, actionGateVerified, sourceNeedsLoad, beginSourceRefresh, completeSourceRefresh, pickProviderQuestion, matchInputs, readFormAnswer, readInputProfile, formAcceptanceTrace, resolveFormInputs, formInputApplies, formInputFilled, nextFormInput, formInputChain, openFormInstance, validateFormIncompleteOutcome, formIncompleteOutcome, validateFormExhaustion, exhaustFormInput, formInitialReadings, orderFormReadings, proposedFormValues, applyFormValidation, formReadingDependencies, exclusiveFormReadings, enrichFormMatches, expandFormActivations, settleFormActivations, rememberFormArguments, resetFormInputs, hasFormValue, formPath, formValuesEqual, formReadingReady, formReadingRelevant, formPreparation, queueFormReadings, pendingFormReadings, formWorkLimit, answerResetsFormReplays, rejectFormReadings, invalidateFormInputs, acceptFormReadings, formInstanceApplies, formParentResume, closeFormInstance, checkOtp, maskDestination, validateOTP, OTP_EXPIRY_MS, validateDigits, validatePhone, validateEmail, urlToSpeech, textWithUrlToSpeech, };
|
|
38
|
+
export { continuationSafetyProtocol, cancelFormTask, contextualResponseProtocol, validateFormResponse, hasFormResponse, formProposalPolicy, stageFormProposal, nextFormProposal, formProposalReadings, bindFormProposal, decideFormProposal, commitFormProposals, settleFormProposalValidation, formProposalProtocol, validateFormProposalPolicy, validateFormProposalDescriptor, formProposalDescriptor, clearFormMisses, formNoOwner, missFormInput, formMustComplete, formInputPrompt, consumeFormFeedback, formConversationOutcome, readMoneyAmount, isMoneyAmount, moneyIntentAllows, isPastPaymentStatement, isPartialMoneyAnswer, validateMoneyOptions, takeChoiceOffer, matchesChoice, ANSWER_WORDS, CONFIRM_WORDS, isAnswer, isAgentRequest, isExitRequest, isOtherRequest, isSpelledFragment, isAmountAnswer, amountInAnswer, pickChoice, readEmail, readPhone, readInternationalPhone, readIdentifier, last4, maskEmail, callerTextNumber, textDestination, callerIsInternational, normalizeAndFindCapture, anythingElse, goodbye, sayName, dateToWords, amountToSpeech, validateCardNumber, validateExpiration, consumeActionReceipt, authorizeFormActionRetry, formSignature, selectFormAction, confirmFormRevision, prepareFormAction, actionConfirmationValid, beginFormAction, finishFormAction, actionGateVerified, sourceNeedsLoad, beginSourceRefresh, completeSourceRefresh, pickProviderQuestion, matchInputs, readFormAnswer, readInputProfile, formAcceptanceTrace, resolveFormInputs, formInputApplies, formInputFilled, nextFormInput, formInputChain, openFormInstance, validateFormIncompleteOutcome, formIncompleteOutcome, validateFormExhaustion, exhaustFormInput, formInitialReadings, orderFormReadings, proposedFormValues, applyFormValidation, formReadingDependencies, exclusiveFormReadings, enrichFormMatches, expandFormActivations, settleFormActivations, rememberFormArguments, resetFormInputs, hasFormValue, formPath, formValuesEqual, formReadingReady, formReadingRelevant, formPreparation, queueFormReadings, pendingFormReadings, formWorkLimit, answerResetsFormReplays, rejectFormReadings, invalidateFormInputs, acceptFormReadings, formInstanceApplies, formParentResume, closeFormInstance, checkOtp, maskDestination, validateOTP, OTP_EXPIRY_MS, validateDigits, validatePhone, validateEmail, urlToSpeech, textWithUrlToSpeech, };
|
|
39
39
|
/** The functions flows call by name, ready to spread into a host's approved functions. */
|
|
40
40
|
export const STANDARD_FUNCTIONS = {
|
|
41
41
|
continuationSafetyProtocol, cancelFormTask, contextualResponseProtocol, validateFormResponse, hasFormResponse,
|
|
42
42
|
formProposalPolicy, stageFormProposal, nextFormProposal, formProposalReadings, bindFormProposal, decideFormProposal, commitFormProposals, settleFormProposalValidation, formProposalProtocol, validateFormProposalPolicy, validateFormProposalDescriptor, formProposalDescriptor,
|
|
43
|
-
clearFormMisses, missFormInput, formMustComplete, formInputPrompt, consumeFormFeedback, formConversationOutcome,
|
|
43
|
+
clearFormMisses, formNoOwner, missFormInput, formMustComplete, formInputPrompt, consumeFormFeedback, formConversationOutcome,
|
|
44
44
|
readMoneyAmount, isMoneyAmount, moneyIntentAllows, isPastPaymentStatement, isPartialMoneyAnswer, validateMoneyOptions, takeChoiceOffer, matchesChoice, isAnswer, isAgentRequest, isExitRequest, isOtherRequest, isSpelledFragment, isAmountAnswer, amountInAnswer, pickChoice,
|
|
45
45
|
readEmail, readPhone, readInternationalPhone, readIdentifier, last4, maskEmail, callerTextNumber, textDestination, callerIsInternational,
|
|
46
46
|
normalizeAndFindCapture, anythingElse, goodbye, sayName, dateToWords, amountToSpeech, validateCardNumber, validateExpiration, consumeActionReceipt, authorizeFormActionRetry, formSignature, selectFormAction, confirmFormRevision, prepareFormAction, actionConfirmationValid, beginFormAction, finishFormAction, actionGateVerified, sourceNeedsLoad, beginSourceRefresh, completeSourceRefresh, pickProviderQuestion, matchInputs, readFormAnswer, readInputProfile, formAcceptanceTrace, resolveFormInputs, formInputApplies, formInputFilled, nextFormInput, formInputChain, openFormInstance, validateFormIncompleteOutcome, formIncompleteOutcome, validateFormExhaustion, exhaustFormInput, formInitialReadings, orderFormReadings, proposedFormValues, applyFormValidation, formReadingDependencies, exclusiveFormReadings, enrichFormMatches, expandFormActivations, settleFormActivations, rememberFormArguments, resetFormInputs, hasFormValue, formPath, formValuesEqual, formReadingReady, formReadingRelevant, formPreparation, queueFormReadings, pendingFormReadings, formWorkLimit, answerResetsFormReplays, rejectFormReadings, invalidateFormInputs, acceptFormReadings, formInstanceApplies, formParentResume, closeFormInstance,
|
|
@@ -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;AAiFD;;;GAGG;AACH,sFA0GC;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH;mHACmH;AACnH;;;;;;;;;;;;;;;;;;;;;EAeC;AACD;;;;;;;;;;;;;;;;;;;;;;EAoOC"}
|
|
@@ -37,6 +37,17 @@ function unescape(answer) {
|
|
|
37
37
|
function digitRuns(raw) {
|
|
38
38
|
return (raw.match(/\d(?:[\s.-]?\d)*/g) || []).map((r) => r.replace(/\D/g, ''));
|
|
39
39
|
}
|
|
40
|
+
const DECIMAL_AMOUNT = /(?:[$€£]\s?)?\d+[.,]\d{2}(?!\d)/g; // a written decimal amount, with its currency mark
|
|
41
|
+
/** Where the answer says amount n: a written decimal by itself ("51 72.45" is not one spoken run), else its number spans. */
|
|
42
|
+
function amountSpans(raw, n) {
|
|
43
|
+
const decimals = [...raw.matchAll(DECIMAL_AMOUNT)].filter((m) => Number(m[0].replace(/[^\d.,]/g, '').replace(',', '.')) === n).map((m) => [m.index, m.index + m[0].length]);
|
|
44
|
+
return decimals.length ? decimals : numberSpans(raw, n, true);
|
|
45
|
+
}
|
|
46
|
+
/** The answer with its amount's own digits blanked (same length, so spans still index the original). */
|
|
47
|
+
function amountMasked(raw) {
|
|
48
|
+
const spans = amountSpans(raw, amountInAnswer(raw));
|
|
49
|
+
return spans.reduce((text, [a, b]) => text.slice(0, a) + ' '.repeat(b - a) + text.slice(b), raw);
|
|
50
|
+
}
|
|
40
51
|
/** Source intervals let different readers recognize the same evidence without agreeing on its type. */
|
|
41
52
|
function numberSpans(raw, value, amount = false) {
|
|
42
53
|
const runs = [...raw.matchAll(/\d(?:[\s.,-]?\d)*/g)];
|
|
@@ -117,19 +128,19 @@ export function readInputProfile(profile, answer, isCurrent = true) {
|
|
|
117
128
|
}
|
|
118
129
|
const kind = profile && profile.kind;
|
|
119
130
|
if (kind === 'choice') {
|
|
131
|
+
// An amount's own digits are the amount's: a number beside it ("51:72.45", sub account 51 and $72.45) is still a choice.
|
|
132
|
+
const free = isAmountAnswer(raw) ? amountMasked(raw) : raw;
|
|
133
|
+
const runs = digitRuns(free), joined = free.replace(/\d(?:[\s.-]?\d)*/g, run => ' ' + run.replace(/\D/g, '') + ' ');
|
|
120
134
|
const choices = (Array.isArray(profile.choices) ? profile.choices : []).filter(c => isCurrent || c.current_only !== true).map(c => ({ ...c,
|
|
121
135
|
numericWords: (c.phrases ?? c.words ?? []).filter(word => /^\d{2,}$/.test(String(word).trim())),
|
|
122
|
-
phrases: (c.phrases ?? c.words ?? []).concat(isCurrent ? c.keys || [] : []).filter(word => !(
|
|
136
|
+
phrases: (c.phrases ?? c.words ?? []).concat(isCurrent ? c.keys || [] : []).filter(word => !(free !== raw && /^\d+$/.test(String(word).trim()) && !runs.includes(String(word).trim())) && (isCurrent || profile.numeric_values === true || !/^[\d*#]$/.test(String(word).trim()))) }));
|
|
123
137
|
// Semantic numeric values may arrive in ASR groups ("95 52"), unlike menu keys.
|
|
124
138
|
// Keep choice policy in pickChoice and source evidence in the original utterance.
|
|
125
|
-
const runs = digitRuns(raw), joined = raw.replace(/\d(?:[\s.-]?\d)*/g, run => ' ' + run.replace(/\D/g, '') + ' ');
|
|
126
139
|
const numericSpans = c => {
|
|
127
|
-
if (isAmountAnswer(raw))
|
|
128
|
-
return [];
|
|
129
140
|
const words = c.numericWords.filter(word => runs.includes(String(word).trim()));
|
|
130
141
|
if (!words.length || pickChoice(joined, [{ ...c, phrases: words }], false) !== c.key)
|
|
131
142
|
return [];
|
|
132
|
-
return words.flatMap(word => numberSpans(
|
|
143
|
+
return words.flatMap(word => numberSpans(free, String(word).trim()).filter(([a, b]) => free.slice(a, b).replace(/\D/g, '') === String(word).trim()));
|
|
133
144
|
};
|
|
134
145
|
const base = pickChoice(raw, choices, false);
|
|
135
146
|
const negative = c => ['no', 'decline', 'done', 'cancel'].includes(c.key) || c.negative === true;
|
|
@@ -160,7 +171,7 @@ export function readInputProfile(profile, answer, isCurrent = true) {
|
|
|
160
171
|
if (inRange) {
|
|
161
172
|
// the digits it was read from: the runs that start with its whole-dollar part ("82.50" said as "82 50" → 8250)
|
|
162
173
|
const runs = digitRuns(raw).filter((d) => d.indexOf(String(Math.trunc(n))) === 0);
|
|
163
|
-
return { value: String(n), spans: [...
|
|
174
|
+
return { value: String(n), spans: [...amountSpans(raw, n), ...(money?.cardinals ? readSpokenMoney(raw).filter(reading => reading.value === n).map(reading => [reading.start, reading.end]) : [])], evidence: runs.length ? runs.map((d) => `d:${d}`) : ['amount'] };
|
|
164
175
|
}
|
|
165
176
|
else {
|
|
166
177
|
return null;
|
|
@@ -196,7 +207,8 @@ export function readInputProfile(profile, answer, isCurrent = true) {
|
|
|
196
207
|
else if (kind === 'digits') {
|
|
197
208
|
const min = Number(profile.min || 1);
|
|
198
209
|
const max = Number(profile.max || min);
|
|
199
|
-
|
|
210
|
+
// Out of order a decimal amount ("72.45", "287,15") is never this field's digits.
|
|
211
|
+
const fits = [...new Set(digitRuns(isCurrent ? raw : raw.replace(DECIMAL_AMOUNT, (m) => ' '.repeat(m.length))).filter((d) => d.length >= min && d.length <= max))];
|
|
200
212
|
if (fits.length === 1) {
|
|
201
213
|
return { value: fits[0], spans: numberSpans(raw, fits[0]), evidence: [`d:${fits[0]}`] };
|
|
202
214
|
}
|
|
@@ -344,11 +356,15 @@ export function matchInputs(answer, candidates, current, choiceKey) {
|
|
|
344
356
|
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)));
|
|
345
357
|
const inOrder = (reading) => reading.current || invited(reading) || pairedWithCurrent(reading);
|
|
346
358
|
// Rule 3: a number read out of order must be whole: every digit run its evidence touches lies entirely inside it,
|
|
347
|
-
// so the last four of "88 91 7112" fill nothing.
|
|
348
|
-
const
|
|
359
|
+
// 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).
|
|
360
|
+
const decimalRuns = [...raw.matchAll(DECIMAL_AMOUNT)].map((m) => [m.index, m.index + m[0].length]);
|
|
361
|
+
const undecimal = decimalRuns.reduce((text, [a, b]) => text.slice(0, a) + ' '.repeat(b - a) + text.slice(b), raw);
|
|
362
|
+
const runs = [...undecimal.matchAll(/\d(?:[\s.-]?\d)*/g)].map((m) => [m.index, m.index + m[0].replace(/\s+$/, '').length]).concat(decimalRuns);
|
|
349
363
|
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));
|
|
350
|
-
// Rule 4: a bare number has no money mark and no word in its evidence.
|
|
351
|
-
|
|
364
|
+
// Rule 4: a bare number has no money mark and no word in its evidence. Written cents are a money mark: "51:72.45" at
|
|
365
|
+
// the sub account question is sub account 51 and the amount 72.45 (prod 2026-10-30).
|
|
366
|
+
const writtenCents = (reading) => (reading.spans || []).every(([a, b]) => decimalRuns.some(([c, d]) => c <= a && b <= d));
|
|
367
|
+
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))) && !writtenCents(reading);
|
|
352
368
|
// Rule 1 by shape: a bare number whose length is the current question's own keypad length (`digits`) answers that
|
|
353
369
|
// question even when its reader rejects it (an expiration "1320" is month 13); its validation decides and the
|
|
354
370
|
// question is asked again. It is never read out of order as another field (that "1320" is not a security code).
|
|
@@ -388,6 +404,37 @@ export function matchInputs(answer, candidates, current, choiceKey) {
|
|
|
388
404
|
return true;
|
|
389
405
|
}
|
|
390
406
|
});
|
|
407
|
+
// Rule 4 for a compound answer: a bare number beside other words ("New card 100") is the invited amount even when the
|
|
408
|
+
// amount reader, which reads only a whole monetary answer, could not read it from the whole answer.
|
|
409
|
+
for (const name of invitedNames) {
|
|
410
|
+
const field = list.find((c) => c.name === name && c.profile.kind === 'amount' && !isCur(c));
|
|
411
|
+
const other = field && !readings.some((reading) => reading.form === field.form && reading.name === name) ? readings.find((reading) => reading.rule === 'out_of_order' && bare(reading)) : null;
|
|
412
|
+
const read = other ? readInputProfile(field.profile, spanText(other), true) : null;
|
|
413
|
+
if (read && !read.alternatives && !read.incompleteMoney) {
|
|
414
|
+
readings.push({ ...other, form: field.form, name: field.name, value: read.value, known: false, repeatedChoice: false, wholeChoice: false, choice: false, narrative: false, resetOn: [], keypad: false,
|
|
415
|
+
group: field.group, exclusive: field.exclusive === true, level: forms.indexOf(field.scope || field.form), current: false, confirm: Array.isArray(field.confirm_against) ? field.confirm_against : [], rule: 'invited' });
|
|
416
|
+
}
|
|
417
|
+
else {
|
|
418
|
+
// no invited amount, or it was read, or no bare number for it
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
// Written cents beside another field's reading ("51:72.45" at the sub account question): the cents are the open amount,
|
|
422
|
+
// read alone, since the amount reader reads only a whole monetary answer. Never alone: "I paid 120.00" is no answer.
|
|
423
|
+
const centsField = list.filter((c) => c.profile.kind === 'amount' && !readings.some((reading) => reading.form === c.form && reading.name === c.name));
|
|
424
|
+
const uncovered = decimalRuns.filter(([a, b]) => !readings.some((reading) => (reading.spans || []).some(([s, e]) => s < b && a < e)));
|
|
425
|
+
if (centsField.length === 1 && uncovered.length === 1 && decimalRuns.length === 1 && readings.some((reading) => (reading.spans || []).length)) {
|
|
426
|
+
const field = centsField[0], [a, b] = uncovered[0], read = readInputProfile(field.profile, raw.slice(a, b), isCur(field));
|
|
427
|
+
if (read && !read.alternatives && !read.incompleteMoney) {
|
|
428
|
+
readings.push({ form: field.form, name: field.name, value: read.value, known: false, repeatedChoice: false, wholeChoice: false, choice: false, narrative: false, resetOn: [], keypad: false, spans: [[a, b]], fallback: false,
|
|
429
|
+
group: field.group, exclusive: field.exclusive === true, level: forms.indexOf(field.scope || field.form), current: isCur(field), confirm: Array.isArray(field.confirm_against) ? field.confirm_against : [], rule: isCur(field) ? undefined : 'out_of_order' });
|
|
430
|
+
}
|
|
431
|
+
else {
|
|
432
|
+
// the cents are not an amount this field takes
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
else {
|
|
436
|
+
// no single open amount, no single uncovered written decimal, or nothing else read
|
|
437
|
+
}
|
|
391
438
|
// Rule 4: a single digit the current question offers as a key is that key (the keypad reading wins its group below);
|
|
392
439
|
// one it does not offer reaches the invited amount, whose own validation decides ("3 dollars?").
|
|
393
440
|
const paired = (a, b) => a.confirm.indexOf(b.name) > -1 || b.confirm.indexOf(a.name) > -1;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"money.d.ts","sourceRoot":"","sources":["../../src/helpers/money.js"],"names":[],"mappings":"AAIA,gHAAgH;AAChH,wDAGC;
|
|
1
|
+
{"version":3,"file":"money.d.ts","sourceRoot":"","sources":["../../src/helpers/money.js"],"names":[],"mappings":"AAIA,gHAAgH;AAChH,wDAGC;AAqBD;8FAC8F;AAC9F,oEAIC;AACD,+FAA+F;AAC/F,0DAEC;AACD;oGACoG;AACpG,4DAGC;AAWD;;;GAGG;AACH,+DAYC;AACD,4FAA4F;AAC5F,iEAAsG"}
|
package/dist/helpers/money.js
CHANGED
|
@@ -10,8 +10,11 @@ export function validateMoneyOptions(options) {
|
|
|
10
10
|
// This is an input grammar, not a conversation classifier. Anything outside one complete
|
|
11
11
|
// monetary answer belongs to DISPATCH, including refusals, corrections with context and reports. "157 95" is dollars and
|
|
12
12
|
// cents said with no word between them (owner 2026-10-29); "95 52" is not (a card's last four read in pairs).
|
|
13
|
-
const NUMERIC_MONEY = /^(?:\$\s*)?(?:\d{1,3}(?:[,.]\d{3})+|\d+)(?:[.,]\d{1,2})?(?:\s*(?:dollars?|d[oó]lar(?:es)?|bucks
|
|
13
|
+
const NUMERIC_MONEY = /^(?:\$\s*)?(?:\d{1,3}(?:[,.]\d{3})+|\d+)(?:[.,]\d{1,2})?(?:\s*(?:dollars?|d[oó]lar(?:es)?|bucks?|\$))?(?:\s*(?:and|con|y)\s*(?:un\s*)?\d{1,2}(?:\s*(?:cents?|centavos?))?)?$|^\d{1,2}\s*(?:cents?|centavos?)$|^(?:\$\s*)?(?:\d|\d{3,})\s+\d{2}$/i;
|
|
14
14
|
const PAYMENT_PREFIX = /^(?:(?:i (?:want to|would like to|can|am able to|will) |i['’]d like to )?pay|(?:quiero |quisiera |puedo |s[ií] puedo |voy a )?(?:pagar|abonar)|(?:no,?\s+)?make it)\s*(?=\$|\d|[a-záéíóúüñ])/i;
|
|
15
|
+
// The field named with its value ("100 amount", "the amount is 100", "monto 100"): the amount, said as such.
|
|
16
|
+
const AMOUNT_LABEL_PREFIX = /^(?:(?:the|el|la)\s+)?(?:amount|monto|cantidad)(?:\s+(?:is|of|es|de))?[:\s]+(?=\$|\d)/i;
|
|
17
|
+
const AMOUNT_LABEL_SUFFIX = /(?<=\d|\$)\s+(?:(?:as\s+)?(?:the\s+)?amount|(?:de\s+|el\s+)?monto|(?:de\s+|la\s+)?cantidad)$/i;
|
|
15
18
|
const ACCOUNT_PREFIX = /^(?:(?:sub[ -]?account|subcuenta|cuenta)\s+\d{1,8}\s*,\s*)/i;
|
|
16
19
|
const DIGIT_WORD = '(?:zero|one|two|three|four|five|six|seven|eight|nine|cero|uno|dos|tres|cuatro|cinco|seis|siete|ocho|nueve)';
|
|
17
20
|
const CARD_SUFFIX = new RegExp('\\s+(?:with (?:the |my )?card(?: (?:ending(?: in)?|number))?|con (?:la |mi )?tarjeta(?: (?:que termina en|terminad[ao] en))?)\\s+(?:\\d(?:[ -]?\\d){3}|' + DIGIT_WORD + '(?: ' + DIGIT_WORD + '){3})$', 'i');
|
|
@@ -19,7 +22,7 @@ const ACCOUNT_SUFFIX = /\s+(?:(?:on|to) (?:sub[ -]?account|account)|(?:a|en) (?:
|
|
|
19
22
|
function completeMoneyAnswer(input) {
|
|
20
23
|
let raw = String(input ?? '').replace(/\\(['"\\])/g, '$1').trim().replace(/^[¡!]+|[.!]+$/g, '').trim();
|
|
21
24
|
raw = raw.replace(/^(?:please|por favor)[, ]+/i, '').replace(/[, ]+(?:please|por favor)$/i, '').trim();
|
|
22
|
-
raw = raw.replace(PAYMENT_PREFIX, '').replace(ACCOUNT_PREFIX, '').replace(CARD_SUFFIX, '').replace(ACCOUNT_SUFFIX, '').trim();
|
|
25
|
+
raw = raw.replace(PAYMENT_PREFIX, '').replace(AMOUNT_LABEL_PREFIX, '').replace(AMOUNT_LABEL_SUFFIX, '').replace(ACCOUNT_PREFIX, '').replace(CARD_SUFFIX, '').replace(ACCOUNT_SUFFIX, '').trim();
|
|
23
26
|
if (NUMERIC_MONEY.test(raw))
|
|
24
27
|
return true;
|
|
25
28
|
const { readings, invalid } = scanSpokenMoney(raw);
|
|
@@ -23,6 +23,9 @@
|
|
|
23
23
|
import type { FlowParameterDefinition } from '../index.js';
|
|
24
24
|
/** A noul at or above this is a yes. */
|
|
25
25
|
export declare const STAGE_YES_THRESHOLD = 0.5;
|
|
26
|
+
/** Stage 1: a start below this (`any`) whose route gives its top flow less than SPLIT_ROUTE_SHARE is "None" (owner 2026-10-29). */
|
|
27
|
+
export declare const WEAK_START = 0.55;
|
|
28
|
+
export declare const SPLIT_ROUTE_SHARE = 0.7;
|
|
26
29
|
export interface StageTurn {
|
|
27
30
|
role: 'user' | 'assistant';
|
|
28
31
|
content: string;
|
|
@@ -112,7 +115,9 @@ export declare function decodeFlowLlm(text: string, stage: FlowStageRequest): {
|
|
|
112
115
|
};
|
|
113
116
|
/**
|
|
114
117
|
* System One's stage-1 answers as the canonical reply. `any` at or above the threshold starts the flow `route`
|
|
115
|
-
* chose; below it, "None". `
|
|
118
|
+
* chose; below it, "None". A weak start (`any` below WEAK_START) is "None" too when the route is split, its top flow
|
|
119
|
+
* under SPLIT_ROUTE_SHARE: unsure a flow is wanted and unsure which, the Guru answers (prod 2026-10-29: 'Esta
|
|
120
|
+
* cancelación afecta mi crédito?' after a cancellation, any 0.51, restarted CancelService). `scores` are the top three flows by the chance that each is the one started,
|
|
116
121
|
* any × its route probability (for logs and the host's near-miss hedge); `any` is logged with them. Throws when
|
|
117
122
|
* either answer is missing or names no offered flow.
|
|
118
123
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"intent-stages.d.ts","sourceRoot":"","sources":["../../src/host/intent-stages.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,aAAa,CAAC;AAE3D,wCAAwC;AACxC,eAAO,MAAM,mBAAmB,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"intent-stages.d.ts","sourceRoot":"","sources":["../../src/host/intent-stages.ts"],"names":[],"mappings":"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"}
|
|
@@ -22,6 +22,9 @@
|
|
|
22
22
|
*/
|
|
23
23
|
/** A noul at or above this is a yes. */
|
|
24
24
|
export const STAGE_YES_THRESHOLD = 0.5;
|
|
25
|
+
/** Stage 1: a start below this (`any`) whose route gives its top flow less than SPLIT_ROUTE_SHARE is "None" (owner 2026-10-29). */
|
|
26
|
+
export const WEAK_START = 0.55;
|
|
27
|
+
export const SPLIT_ROUTE_SHARE = 0.7;
|
|
25
28
|
// ── the rules: written once, rendered for both executors ─────────────────────────────────────────────
|
|
26
29
|
const FLOW_TRUE = "The latest message, read with the conversation, is what the workflow's description says should start it: the user asks for its action or information, or raises the problem it exists to handle. Also when it accepts the assistant's offer in the conversation to start or resume it (assistant: \"Shall I try the payment again?\" → user: \"yes\").";
|
|
27
30
|
const FLOW_FALSE = "It only shares a topic with the workflow, is not what the workflow's description covers, or is a greeting, an acknowledgement or a fragment that asks for nothing.";
|
|
@@ -31,6 +34,8 @@ const PARAMETER_RULES = [
|
|
|
31
34
|
'Give a value only when the user gave it, in the latest message or earlier in the conversation. Never guess.',
|
|
32
35
|
"Write it as the parameter's rule says; otherwise the value alone, in the user's words (never the whole sentence around it).",
|
|
33
36
|
'A short answer to the assistant\'s last message ("1", "yes", "the first one") chooses what that message offered.',
|
|
37
|
+
'A plain yes to the assistant\'s last message, when it asked the user to confirm values, gives those values as the user\'s own (assistant: "You want to pay $40 to sub account 80?" → user: "yes": 40 and 80; "¿Desea pagar $40 a la subcuenta 80?" → "sí": 40 and 80). A yes to a question that confirmed no value gives none ("Your payment of $40 was successful. Anything else?" → "yes": none).',
|
|
38
|
+
'When the user gave a value more than once, the latest one counts.',
|
|
34
39
|
'A value the user did not give is null.',
|
|
35
40
|
];
|
|
36
41
|
const ENUM_TRUE = (value) => `The parameter's rule makes \`${value}\` its value for what the user said.`;
|
|
@@ -110,7 +115,9 @@ export function decodeFlowLlm(text, stage) {
|
|
|
110
115
|
}
|
|
111
116
|
/**
|
|
112
117
|
* System One's stage-1 answers as the canonical reply. `any` at or above the threshold starts the flow `route`
|
|
113
|
-
* chose; below it, "None". `
|
|
118
|
+
* chose; below it, "None". A weak start (`any` below WEAK_START) is "None" too when the route is split, its top flow
|
|
119
|
+
* under SPLIT_ROUTE_SHARE: unsure a flow is wanted and unsure which, the Guru answers (prod 2026-10-29: 'Esta
|
|
120
|
+
* cancelación afecta mi crédito?' after a cancellation, any 0.51, restarted CancelService). `scores` are the top three flows by the chance that each is the one started,
|
|
114
121
|
* any × its route probability (for logs and the host's near-miss hedge); `any` is logged with them. Throws when
|
|
115
122
|
* either answer is missing or names no offered flow.
|
|
116
123
|
*/
|
|
@@ -128,7 +135,8 @@ export function decodeFlowSystemOne(answers, stage, { threshold = STAGE_YES_THRE
|
|
|
128
135
|
const probabilities = Object.fromEntries(names.map(n => [n, Number(raw[n]) || 0]));
|
|
129
136
|
const ranked = names.map(n => ({ name: n, p: any * probabilities[n] })).sort((a, b) => b.p - a.p);
|
|
130
137
|
const scores = ranked.slice(0, 3);
|
|
131
|
-
const
|
|
138
|
+
const split = any < WEAK_START && !!ranked[0] && probabilities[ranked[0].name] < SPLIT_ROUTE_SHARE;
|
|
139
|
+
const flowName = any >= threshold && ranked[0] && !split ? ranked[0].name : 'None';
|
|
132
140
|
return { flowName, scores, any };
|
|
133
141
|
}
|
|
134
142
|
/**
|
package/dist/runtime-identity.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const JSFE_RUNTIME_FINGERPRINT = "sha256:
|
|
1
|
+
export const JSFE_RUNTIME_FINGERPRINT = "sha256:fd2a9a936f409b3e5eaf40eb2c60a9f081caa099852cbd8fabdb5135a862d5f2";
|
|
@@ -132,6 +132,7 @@ A form declaration has top-level common fields and `form:{inputs:[...]}`. Do not
|
|
|
132
132
|
| `prompt`, `prompt_es` | UI labels. |
|
|
133
133
|
| `incomplete_outcome` | Explicit business outcome fallback for incomplete optional followups; see recovery. |
|
|
134
134
|
| `on_exhausted` | Bounded recovery worker after validation attempts are exhausted. |
|
|
135
|
+
| `on_no` | Worker (called with `{form}`) for a whole NO at one of the form's questions, or a sub-form's, that the question does not read itself (not a yes/no, confirmation or proposal question, and no choice of it reads NO); the form then continues. A payment declares one that starts its selection again. |
|
|
135
136
|
|
|
136
137
|
Top-level `variables` provides extra form locals. JSFE creates instance IDs, saved arguments, values, validation and confirmation state; do not author copies of these bookkeeping fields.
|
|
137
138
|
|
|
@@ -255,6 +256,8 @@ An unreadable answer with any words goes to the reply ranker, offered the questi
|
|
|
255
256
|
|
|
256
257
|
`must_complete:true` on a form (inherited by every field of it and of the forms nested in it) or on one field (`false` on a field opts out): an unreadable answer is never yielded to DISPATCH, however long, and neither is a confirmation miss or an unresolved ambiguity — the question is asked again, acknowledged. Use it where leaving would strand an effect in progress (a payment's card, its billing-ZIP correction). Exits stay explicit: the form's stop words, `max_attempts` with `on_exhausted`, and whatever the host intercepts before the engine. The form runaway guard counts passes without a caller answer only.
|
|
257
258
|
|
|
259
|
+
A choice marked `current_only` is read only at its own question: neither the word reader nor the reply ranker offers it while another question is asked (a route's `done` or `switch` is never picked for a NO or a sub account said at the amount question). Written cents (`72.45`, `$72.45`, `287,15`) are an amount by shape: out of order they fill an open amount, and beside another field's reading (`51:72.45` at a sub account question) they are that amount, never digits of a security code or ZIP code.
|
|
260
|
+
|
|
258
261
|
Use `response:{description}` for open descriptions that need a model's interpretation. The prepared question and field description become the contextual DISPATCH question, and the reply ranker decides what the reply does: an answer is extracted and still passes deterministic validation (missing usable data yields with progress saved); a withdrawal leaves for the host; a new request routes through intent detection. No separate field classifier/model role is needed. Do not attempt exhaustive conversational inference with keyword lists.
|
|
259
262
|
|
|
260
263
|
Ordinary DISPATCH is the default uncertainty escape. An explicit contextual DISPATCH may additionally declare the following step fragment (its surrounding flow must declare the response object and supply its own input/recovery steps):
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jsfe",
|
|
3
|
-
"version": "1.15.
|
|
3
|
+
"version": "1.15.9",
|
|
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",
|