jsfe 1.10.3 → 1.10.5

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.
@@ -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']);
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']);
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); };
@@ -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 }: {
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 }: {
2
2
  id: any;
3
3
  name: any;
4
4
  version: any;
@@ -23,6 +23,7 @@ export function formFlow({ id, name, version, description, inputs, after, extraV
23
23
  prompt_es?: undefined;
24
24
  incomplete_outcome?: null | undefined;
25
25
  on_exhausted?: null | undefined;
26
+ must_complete?: boolean | undefined;
26
27
  }): {
27
28
  variables: {
28
29
  [k: string]: {
@@ -1 +1 @@
1
- {"version":3,"file":"flows.d.ts","sourceRoot":"","sources":["../../src/forms/flows.js"],"names":[],"mappings":"AAsHA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA2DC;AACD;;;GAGG;AACH;;;;;;EAGC;AAyYD,wHAAwH;AACxH,wEAaC;AACD,4FAA4F;AAC5F;;;EAYC;AACD,sGAAsG;AACtG;;;EAOC;AA7jBM,6CAAuG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAovB9G;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAOG"}
1
+ {"version":3,"file":"flows.d.ts","sourceRoot":"","sources":["../../src/forms/flows.js"],"names":[],"mappings":"AAsHA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA4DC;AACD;;;GAGG;AACH;;;;;;EAGC;AAgZD,wHAAwH;AACxH,wEAaC;AACD,4FAA4F;AAC5F;;;EAYC;AACD,sGAAsG;AACtG;;;EAOC;AArkBM,6CAAuG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4vB9G;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAOG"}
@@ -112,8 +112,10 @@ function validateAcceptanceDeclarations(id, inputs) {
112
112
  else { /* no invites, or a list of names */ }
113
113
  }
114
114
  }
115
- 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 }) {
115
+ 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 }) {
116
116
  validateFormExhaustion(on_exhausted);
117
+ if (typeof must_complete !== 'boolean')
118
+ throw new Error(`FORM ${id}: must_complete must be true or false`);
117
119
  validateAcceptanceDeclarations(id, inputs);
118
120
  const incompleteFrom = incomplete_outcome && Object.hasOwn(incomplete_outcome, 'from');
119
121
  if (incompleteFrom) {
@@ -137,13 +139,13 @@ export function formFlow({ id, name, version, description, inputs, after = [], e
137
139
  ] : [];
138
140
  return {
139
141
  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 } : {}),
140
- variables: vars({ form_on_exhausted: on_exhausted, 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 }),
142
+ 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 }),
141
143
  steps: [
142
144
  ...contextualResponseRuntimeSteps(id, [], hasFormResponse(inputs) ? [CONTEXTUAL_RESPONSES_REQUIREMENT] : []),
143
145
  ...(hasFormResponse(inputs) ? [{ ...SET(`${id}_response_reader_required`, 'noop', "typeof validateFormResponse === 'function'"), requireTrue: true }] : []),
144
146
  ...proposalRuntimeSteps(id, [], hasInputProposals(inputs) ? [INPUT_PROPOSALS_REQUIREMENT] : []),
145
147
  SET(`${id}_resume_kind`, 'form_internal_resume', '!!form_instance'),
146
- 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 })`),
148
+ 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 })`),
147
149
  SIDE(`${id}_task`, `(!parent_form ? (cargo.formTasks = cargo.formTasks || {}, cargo.formTasks[${JSON.stringify(id)}] = form_instance) : true, true)`),
148
150
  SIDE(`${id}_provider_reset`, '(delete cargo.forms[form_instance].providerRequests, delete cargo.forms[form_instance].activeGates, delete cargo.forms[form_instance].providerGateActive, true)'),
149
151
  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)'),
@@ -233,7 +235,7 @@ const collectInput = {
233
235
  id: 'collect-input', name: 'CollectInput', version: '1.0.0', scope: 'isolated',
234
236
  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).",
235
237
  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 })),
236
- variables: vars({ ci_partial: '', 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_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: '' }),
238
+ variables: vars({ ci_partial: '', 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: '' }),
237
239
  steps: [
238
240
  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; })()"),
239
241
  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]"),
@@ -242,7 +244,9 @@ const collectInput = {
242
244
  SET('ci_max_attempts', 'ci_max', 'Number(ci_spec.max_attempts || 3)'),
243
245
  SET('ci_context_misses', 'ci_misses', 'Number(((cargo.forms[form] || {}).misses || {})[current] || 0)'),
244
246
  SET('ci_context_limit', 'ci_miss_limit', 'Number(ci_spec.max_misses || 2)'),
245
- WHEN('ci_context_yield', 'ci_misses >= ci_miss_limit', yieldDispatch('ci_misses_dispatch', 'form', "'input_misses_exhausted'")),
247
+ // must_complete (the field's, else the nearest form's): an unreadable answer is never handed away; it is asked again
248
+ SET('ci_must_complete', 'ci_must', 'formMustComplete(cargo,form,current)'),
249
+ WHEN('ci_context_yield', 'ci_misses >= ci_miss_limit && !ci_must', yieldDispatch('ci_misses_dispatch', 'form', "'input_misses_exhausted'")),
246
250
  SET('ci_context', 'ci_ctx', "typeof exit_context !== 'undefined' && exit_context ? exit_context : 'your request'"),
247
251
  SET('ci_context_es', 'ci_ctx_es', "typeof exit_context_es !== 'undefined' && exit_context_es ? exit_context_es : 'su solicitud'"),
248
252
  WHEN('ci_resolve_exhausted', 'ci_missed >= ci_max', SET('ci_exhausted_policy', 'ci_exhaustion', 'exhaustFormInput(cargo,form,current)')),
@@ -269,6 +273,7 @@ const collectInput = {
269
273
  SET('ci_validator', 'ci_val', "ci_spec.answer_validator || ''"),
270
274
  SET('ci_reader_context', 'ci_reader', '({form:form,current:current,names:ci_names,questions:provides && Array.isArray(questions)?questions:[]})'),
271
275
  FLOW('ci_ask', 'ask-choice', 'call', { question: '{{ci_q}}', question_es: '{{ci_q_es}}', choices: '{{ci_choices}}', accept_other: true, digits: '{{ci_digits}}', amount: '{{ci_amount}}', as_spoken: '{{ci_as_spoken}}', pii: '{{ci_pii}}', exit_context: '{{ci_ctx}}', exit_context_es: '{{ci_ctx_es}}', form_context: '{{ci_reader}}', answer_precheck: '{{ci_pre}}', answer_validator: '{{ci_val}}' }),
276
+ SIDE('ci_answered', 'answerResetsFormReplays(cargo,form)'),
272
277
  // a key the input routes to a handler of its own (route_keys: [{ prefix, flow }]): that flow takes the answer
273
278
  SET('ci_route_to', 'ci_routed', "(ci_spec.route_keys || []).filter(function (r) { return String(cargo.choice || '').indexOf(r.prefix) === 0; }).map(function (r) { return r.flow; })[0] || ''"),
274
279
  WHEN('ci_route_key', 'ci_routed', FLOW('ci_route_key_flow', '{{ci_routed}}', 'call', { form: '{{form}}', input: '{{current}}' })),
@@ -296,7 +301,7 @@ const collectInput = {
296
301
  'condition: ci_m.hits.length > 0': FLOW('ci_accept', 'collect-input-accept', 'call', { hits: '{{ci_m.hits}}' }),
297
302
  "condition: ci_m.questions.length > 0": END('ci_question_handled'),
298
303
  "condition: ci_qkey && ci_qkey.indexOf('__') !== 0": FLOW('ci_question_asked', 'provide-data', 'call', { form: '{{form}}', provides: '{{provides}}', asked: '{{ci_qkey}}', no_menu: 'true', no_gate: '{{ci_skip_gate}}' }),
299
- "condition: ci_words >= 3 || ci_spec.profile.kind === 'amount' && ci_words > 0": yieldDispatch('ci_elsewhere', 'form', "'input_not_matched'"),
304
+ "condition: !ci_must && (ci_words >= 3 || ci_spec.profile.kind === 'amount' && ci_words > 0)": yieldDispatch('ci_elsewhere', 'form', "'input_not_matched'"),
300
305
  default: SIDE('ci_miss', "(missFormInput(cargo,form,current),true)")
301
306
  } },
302
307
  ],
@@ -416,7 +421,7 @@ const disambiguate = {
416
421
  id: 'disambiguate-input', name: 'DisambiguateInput', version: '1.0.0', scope: 'isolated',
417
422
  description: "Worker: the answer fits two or more inputs at the same scope (or a declared confirm_against pair). Stores the unambiguous readings, asks which input the rest was for (keys 1..n, never 8), stores the pick. No pick: nothing stored, not a miss. Params: readings, hits.",
418
423
  parameters: [{ name: 'readings', type: 'array', description: 'The ambiguous readings' }, { name: 'hits', type: 'array', description: 'Readings to store as they are' }, { name: 'remaining', type: 'array' }],
419
- variables: vars({ di_form: '', di_input: '', di_prompt: '', di_prompt_es: '', di_words: 0, di_misses: 0, di_keys: [], di_choices: [], di_q: '', di_q_es: '', di_pick: null, di_value: '', remaining: [], di_next: [], di_rest: [] }),
424
+ variables: vars({ di_must: false, di_form: '', di_input: '', di_prompt: '', di_prompt_es: '', di_words: 0, di_misses: 0, di_keys: [], di_choices: [], di_q: '', di_q_es: '', di_pick: null, di_value: '', remaining: [], di_next: [], di_rest: [] }),
420
425
  steps: [
421
426
  WHEN('di_store_clear', 'Array.isArray(hits) && hits.length > 0', FLOW('di_store_hits', 'collect-input-accept', 'call', { hits: '{{hits}}' })),
422
427
  SET('di_number', 'di_keys', `${KEYS_JS}(readings.length)`),
@@ -426,17 +431,19 @@ const disambiguate = {
426
431
  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 ' : 'Say ') + r.label.toUpperCase(); }).join(', ') + '.'"),
427
432
  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 ' : 'Diga ') + r.label_es.toUpperCase(); }).join(', ') + '.'"),
428
433
  SET('di_owner', 'di_form', 'readings[0].form'),
434
+ SET('di_must_complete', 'di_must', 'readings.some(function(r){return formMustComplete(cargo,r.form,r.name);})'),
429
435
  SET('di_counter', 'di_input', "'__ambiguity:'+readings.map(function(r){return r.form+':'+r.name;}).sort().join('|')"),
430
436
  SET('di_prompt', 'di_prompt', "formInputPrompt(cargo,di_form,di_input,{question:di_q,question_es:di_q_es},{},'en')"),
431
437
  SET('di_prompt_es', 'di_prompt_es', "formInputPrompt(cargo,di_form,di_input,{question:di_q,question_es:di_q_es},{},'es')"),
432
438
  SIDE('di_feedback_consumed', 'consumeFormFeedback(cargo,di_form,di_input)'),
433
439
  FLOW('di_ask', 'ask-choice', 'call', { question: '{{di_prompt}}', question_es: '{{di_prompt_es}}', choices: '{{di_choices}}', accept_other: true, digits: { min: 1, max: 1 } }),
440
+ SIDE('di_answered', 'answerResetsFormReplays(cargo,di_form)'),
434
441
  SET('di_picked', 'di_pick', "/^r\\d+$/.test(String(cargo.choice || '')) ? [readings[Number(String(cargo.choice).slice(1))]] : null"),
435
442
  WHEN('di_understood', 'di_pick', SIDE('di_clear_misses', 'clearFormMisses(cargo,di_form,di_input)')),
436
443
  SET('di_word_count', 'di_words', "String(cargo.choiceText || '').split(/\\s+/).filter(function(w){return /[a-záéíóúüñ]/i.test(w);}).length"),
437
- WHEN('di_context', '!di_pick && di_words >= 3', yieldDispatch('di_context_yield', 'di_form', "'ambiguity_not_resolved'")),
444
+ WHEN('di_context', '!di_pick && di_words >= 3 && !di_must', yieldDispatch('di_context_yield', 'di_form', "'ambiguity_not_resolved'")),
438
445
  WHEN('di_miss', '!di_pick', SET('di_miss_count', 'di_misses', 'missFormInput(cargo,di_form,di_input)')),
439
- WHEN('di_exhausted', '!di_pick && di_misses >= 2', yieldDispatch('di_misses_yield', 'di_form', "'ambiguity_misses_exhausted'")),
446
+ WHEN('di_exhausted', '!di_pick && di_misses >= 2 && !di_must', yieldDispatch('di_misses_yield', 'di_form', "'ambiguity_misses_exhausted'")),
440
447
  WHEN('di_retry', '!di_pick', FLOW('di_retry_question', 'disambiguate-input', 'replace', { readings: '{{readings}}', hits: [], remaining: '{{remaining}}' })),
441
448
  WHEN('di_store_pick', 'di_pick', FLOW('di_store_pick_flow', 'collect-input-accept', 'call', { hits: '{{di_pick}}' })),
442
449
  SET('di_remaining_first', 'di_next', 'Array.isArray(remaining) && remaining.length ? remaining[0] : []'),
@@ -449,7 +456,7 @@ const confirmForm = {
449
456
  id: 'confirm-form', name: 'ConfirmForm', version: '1.0.0', scope: 'isolated',
450
457
  description: "Worker: ask a form's confirmation (exact YES / NO; keys 1 and 2). A correction instead ('no, make it $40') is read against the form chain and stored, and the result is 'changed' so the form runs again; an unclear short answer is asked again, a long unrelated one is DISPATCHed. Result: cargo.formConfirm[form] = 'yes' | 'no' | 'changed'. Params: form, question(_es), exit_context(_es).",
451
458
  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" }]),
452
- variables: vars({ named_fields: false, cf_named: null, cf_presets: FIELD_PRESETS, cf_chain: [], cf_m: null, cf_current: null, cf_result: '', cf_prompt: '', cf_prompt_es: '', cf_words: 0, cf_tries: 0, phrases: null, words: [], clarifications: [], no_keys: ['2'], interrupts: [], invites: [], cf_choices: [], cf_event: '', cf_amount: false, cf_questions: [], cf_provides: null, cf_skip_gate: '' }),
459
+ variables: vars({ named_fields: false, cf_must: false, cf_named: null, cf_presets: FIELD_PRESETS, cf_chain: [], cf_m: null, cf_current: null, cf_result: '', cf_prompt: '', cf_prompt_es: '', cf_words: 0, cf_tries: 0, phrases: null, words: [], clarifications: [], no_keys: ['2'], interrupts: [], invites: [], cf_choices: [], cf_event: '', cf_amount: false, cf_questions: [], cf_provides: null, cf_skip_gate: '' }),
453
460
  steps: [
454
461
  SIDE('cf_reset', "(cargo.formConfirm = cargo.formConfirm || {}, cargo.formConfirm[form] = '', true)"),
455
462
  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};}))`),
@@ -459,6 +466,7 @@ const confirmForm = {
459
466
  SET('cf_prompt_es', 'cf_prompt_es', "formInputPrompt(cargo,form,'__confirm',{question:question,question_es:question_es},{},'es')"),
460
467
  SIDE('cf_feedback_consumed', "consumeFormFeedback(cargo,form,'__confirm')"),
461
468
  FLOW('cf_ask', 'ask-choice', 'call', { form_context: { form: '{{form}}', current: '__confirm' }, amount: '{{cf_amount}}', question: '{{cf_prompt}}', question_es: '{{cf_prompt_es}}', choices: '{{cf_choices}}', accept_other: true, reask_long: true, digits: { min: 1, max: 1 }, exit_context: '{{exit_context}}', exit_context_es: '{{exit_context_es}}' }),
469
+ SIDE('cf_answered', 'answerResetsFormReplays(cargo,form)'),
462
470
  SET('cf_event', 'cf_event', "(Array.isArray(interrupts)?interrupts:[]).filter(function(event){return cargo.choice==='event:'+event.key;}).map(function(event){return event.flow;})[0] || ''"),
463
471
  WHEN('cf_interrupt', 'cf_event', FLOW('cf_event_flow', '{{cf_event}}', 'reboot', { form: '{{form}}', event: '{{cargo.choice}}', text: '{{cargo.choiceText}}' })),
464
472
  SET('cf_build_chain', 'cf_chain', CHAIN_JS),
@@ -476,6 +484,7 @@ const confirmForm = {
476
484
  SET('cf_named_field', 'cf_named', "(named_fields === true || named_fields === 'true') && cargo.choice !== 'yes' && !cf_m.hits.length && !cf_m.ambiguous.length && !cf_m.questions.length ? (function (text, chain) { var plain = function (v) { return ' ' + String(v || '').toLowerCase().replace(/[^a-z0-9áéíóúüñ]+/g, ' ').trim() + ' '; }; var said = plain(text); var named = chain.filter(function (input) { return (input.names || []).some(function (name) { return said.indexOf(plain(name)) >= 0; }); }); return named.length === 1 ? { form: named[0].form, name: named[0].name } : null; })(cargo.choiceText, cf_chain) : null"),
477
485
  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}}' })),
478
486
  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}}' })),
487
+ SET('cf_must_complete', 'cf_must', "formMustComplete(cargo,form,'__confirm')"),
479
488
  { id: 'cf_route', type: 'CASE', branches: {
480
489
  'condition: cf_m.questions.length > 0': SET('cf_question_repeat', 'cf_result', "'changed'"),
481
490
  "condition: cargo.choice === 'yes'": SET('cf_answered', 'cf_result', "'yes'"),
@@ -484,13 +493,13 @@ const confirmForm = {
484
493
  'condition: cf_named': SET('cf_named_reset', 'cf_result', "(resetFormInputs(cargo, cf_named.form, [cf_named.name]), 'changed')"),
485
494
  "condition: cargo.choice === 'no'": SET('cf_declined', 'cf_result', "'no'"),
486
495
  "condition: Array.isArray(clarifications) && pickChoice(String(cargo.choiceText || '').replace(/[?¿]/g,''),[{key:'clarify',phrases:clarifications,exact:true}],false)==='clarify'": SET('cf_clarification', 'cf_result', "'clarification'"),
487
- 'condition: cf_words >= 3 || cf_amount && cf_words > 0': yieldDispatch('cf_elsewhere', 'form', "'confirmation_not_matched'"),
496
+ 'condition: !cf_must && (cf_words >= 3 || cf_amount && cf_words > 0)': yieldDispatch('cf_elsewhere', 'form', "'confirmation_not_matched'"),
488
497
  default: SET('cf_unclear', 'cf_result', "'unclear'")
489
498
  } },
490
499
  SET('cf_changed', 'cf_result', "cf_result ? cf_result : 'changed'"),
491
500
  // an unclear short answer is asked again (the form runs again and reaches this question), at most twice
492
501
  SET('cf_tries_count', 'cf_tries', "cf_result === 'unclear' ? missFormInput(cargo,form,'__confirm') : (clearFormMisses(cargo,form,'__confirm'),0)"),
493
- WHEN('cf_misses_exhausted', 'cf_tries >= 2', yieldDispatch('cf_misses_dispatch', 'form', "'confirmation_misses_exhausted'")),
502
+ WHEN('cf_misses_exhausted', 'cf_tries >= 2 && !cf_must', yieldDispatch('cf_misses_dispatch', 'form', "'confirmation_misses_exhausted'")),
494
503
  WHEN('cf_bind_revision', "cf_result === 'yes'", SIDE('cf_bind', 'confirmFormRevision(cargo, form)')),
495
504
  SIDE('cf_store', "(cargo.formConfirm[form] = cf_result === 'unclear' || cf_result === 'clarification' ? 'changed' : cf_result, true)"),
496
505
  ],
@@ -1 +1 @@
1
- {"version":3,"file":"answer.d.ts","sourceRoot":"","sources":["../../src/helpers/answer.js"],"names":[],"mappings":"AAuLA;;;GAGG;AACH,wEAkBC;AAED,+HAA+H;AAC/H,oDAIC;AAcD,2GAA2G;AAC3G,uDAGC;AAOD;;;;;GAKG;AACH,+DAUC;AAKD;;;;GAIG;AACH,oDAEC;AAiBD;;;;;;GAMG;AACH,mDAAmF;AAEnF,kGAAkG;AAClG;;;EAmCC;AAUD;;;;;;;;;GASG;AACH,6EAyGC;;;;;;;;AA1ZD,qCAKiD"}
1
+ {"version":3,"file":"answer.d.ts","sourceRoot":"","sources":["../../src/helpers/answer.js"],"names":[],"mappings":"AA4LA;;;GAGG;AACH,wEAkBC;AAED,+HAA+H;AAC/H,oDAIC;AAcD,2GAA2G;AAC3G,uDAGC;AAOD;;;;;GAKG;AACH,+DAUC;AAKD;;;;GAIG;AACH,oDAEC;AAiBD;;;;;;GAMG;AACH,mDAAmF;AAEnF,kGAAkG;AAClG;;;EAmCC;AAUD;;;;;;;;;GASG;AACH,6EAyGC;;;;;;;;AA/ZD,qCAKiD"}
@@ -176,6 +176,9 @@ function textRequest(input, words, extra) {
176
176
  * when `yesOk`, a YES word ("yes 1", "sí, 1"). Owner decision 2026-10-15: "Yes 1" at "tell me the product number" was
177
177
  * missed. Anything else around the key ("no 1", "1 or 2", "I paid 1 already") is not that key.
178
178
  */
179
+ // Words that name the key itself when said right before it: "el número 4", "opción 2", "number 4", "the 4" (prod
180
+ // 2026-10-25 18:25: "Número 4" and "El número 4" at a numbered product list were not understood, twice).
181
+ const KEY_NAMES = new Set(['number', 'numero', 'option', 'opcion', 'item', 'choice', 'the', 'el', 'la'].map(normalize));
179
182
  function keyWithOnlyCourtesy(b, key, yesOk) {
180
183
  const tk = String(b).replace(/\./g, ' ').split(' ').filter(Boolean);
181
184
  const at = tk.reduce((acc, t, i) => (t === key ? [...acc, i] : acc), []);
@@ -183,7 +186,10 @@ function keyWithOnlyCourtesy(b, key, yesOk) {
183
186
  return false;
184
187
  }
185
188
  else {
186
- const rest = tk.filter((t, i) => i !== at[0]).join(' ');
189
+ let from = at[0];
190
+ while (from > 0 && at[0] - from < 2 && KEY_NAMES.has(tk[from - 1]))
191
+ from--;
192
+ const rest = tk.filter((t, i) => i < from || i > at[0]).join(' ');
187
193
  return onlyAffirmative(rest, yesOk ? ANSWER_WORDS.yes : [], false);
188
194
  }
189
195
  }
@@ -1,5 +1,9 @@
1
1
  export function clearFormMisses(cargo: any, id: any, input: any): boolean;
2
2
  export function missFormInput(cargo: any, id: any, input: any): any;
3
+ /** must_complete: the field's own declaration (true or false) wins, else any enclosing form that declares it. Such an input never hands the
4
+ * turn away on an unreadable answer: it says it did not catch that and asks again. Its exits stay explicit (the form's
5
+ * stop words, max_attempts and on_exhausted, and whatever the host intercepts before the engine). */
6
+ export function formMustComplete(cargo: any, id: any, input: any): boolean;
3
7
  /** Explicit retry wins. Otherwise acknowledge only an actual unrecognized answer, once per re-ask. */
4
8
  export function formInputPrompt(cargo: any, id: any, input: any, spec: any, data?: {}, language?: string): any;
5
9
  export function consumeFormFeedback(cargo: any, id: any, input: any): boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"form-conversation.d.ts","sourceRoot":"","sources":["../../src/helpers/form-conversation.js"],"names":[],"mappings":"AAGA,0EAGC;AACD,oEAGC;AACD,sGAAsG;AACtG,+GAOC;AACD,8EAAwI;AAExI,yGAAyG;AACzG;;;;;;;;;;;;;;;;;;;;EAUC;AAED,sGAAsG;AACtG;;;;;;;;;;;;;;;;;;;;EAeC"}
1
+ {"version":3,"file":"form-conversation.d.ts","sourceRoot":"","sources":["../../src/helpers/form-conversation.js"],"names":[],"mappings":"AAGA,0EAGC;AACD,oEAGC;AACD;;qGAEqG;AACrG,2EAWC;AACD,sGAAsG;AACtG,+GAOC;AACD,8EAAwI;AAExI,yGAAyG;AACzG;;;;;;;;;;;;;;;;;;;;EAUC;AAED,sGAAsG;AACtG;;;;;;;;;;;;;;;;;;;;EAeC"}
@@ -20,6 +20,27 @@ export function missFormInput(cargo, id, input) {
20
20
  state.feedback[input] = 'miss';
21
21
  return state.misses[input];
22
22
  }
23
+ /** must_complete: the field's own declaration (true or false) wins, else any enclosing form that declares it. Such an input never hands the
24
+ * turn away on an unreadable answer: it says it did not catch that and asks again. Its exits stay explicit (the form's
25
+ * stop words, max_attempts and on_exhausted, and whatever the host intercepts before the engine). */
26
+ export function formMustComplete(cargo, id, input) {
27
+ const seen = new Set();
28
+ let name = input;
29
+ while (id && cargo.forms?.[id] && !seen.has(id)) {
30
+ seen.add(id);
31
+ const state = cargo.forms[id];
32
+ const group = (state.specs || []).find(s => s.name === name || s.any_of?.some(i => i.name === name));
33
+ const member = group?.any_of?.find(i => i.name === name);
34
+ for (const declared of [member?.must_complete, group?.must_complete])
35
+ if (typeof declared === 'boolean')
36
+ return declared;
37
+ if (state.must_complete === true)
38
+ return true;
39
+ name = state.result_input;
40
+ id = state.parent;
41
+ }
42
+ return false;
43
+ }
23
44
  /** Explicit retry wins. Otherwise acknowledge only an actual unrecognized answer, once per re-ask. */
24
45
  export function formInputPrompt(cargo, id, input, spec, data = {}, language = 'en') {
25
46
  const es = language === 'es', feedback = cargo.forms?.[id]?.feedback?.[input];
@@ -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 }?: {
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 }?: {
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
+ must_complete?: undefined;
32
33
  }): any;
33
34
  /** An incomplete optional follow-up may preserve an explicitly completed task outcome. */
34
35
  export function validateFormIncompleteOutcome(value: any): {
@@ -82,6 +83,10 @@ export function formPreparation(cargo: any, id: any, input: any): {
82
83
  } | null;
83
84
  export function queueFormReadings(cargo: any, hits?: any[]): boolean;
84
85
  export function pendingFormReadings(cargo: any, id: any): any[];
86
+ /** The caller answered: the runaway guard (replays > formWorkLimit) counts passes WITHOUT an answer, which is what an
87
+ * internal loop is, so an answer resets it on the answering form and every ancestor. Counting answers too ended a form
88
+ * whose caller simply needed several tries (a must_complete field re-asks every unreadable answer). */
89
+ export function answerResetsFormReplays(cargo: any, id: any): boolean;
85
90
  export function formWorkLimit(cargo: any, id: any): number;
86
91
  export function applyFormValidation(cargo: any, id: any, result: any): "failed" | "valid" | "retry";
87
92
  /** Invalidation is transitive and independent of declaration order. */
@@ -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,oEA6BC;AAuCD,2FAEC;AACD,8FAcC;AACD,2DAOC;AACD,uEA0CC;AAED,kGAAkG;AAClG,2GAOC;AACD,0HAA0H;AAC1H,uEAwBC;AACD,8HAA8H;AAC9H;;;;;EAWC;AACD,uGAAuG;AACvG;;;;;;;;;;;QAgCC;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,2DAQC;AACD,oGAUC;AAED,uEAAuE;AACvE,wHA+BC;AAED,uHAAuH;AACvH,6EAaC;AACD,mHAAmH;AACnH,iGAsDC;AACD;;;GAGG;AACH;;;SAcC;AACD,+EASC;AACD,8EAeC;AAnoBM,8CAAmE"}
1
+ {"version":3,"file":"form-state.d.ts","sourceRoot":"","sources":["../../src/helpers/form-state.js"],"names":[],"mappings":"AAQA,qDAKC;AAED,oDAEC;AAED,qFAAqF;AACrF,oEA8BC;AAuCD,2FAEC;AACD,8FAcC;AACD,2DAOC;AACD,uEA0CC;AAED,kGAAkG;AAClG,2GAOC;AACD,0HAA0H;AAC1H,uEAwBC;AACD,8HAA8H;AAC9H;;;;;EAWC;AACD,uGAAuG;AACvG;;;;;;;;;;;;QAkCC;AAED,0FAA0F;AAC1F;;;SAIC;AACD,gGAAgG;AAChG;;;;;;;;;;;EAQC;AAED,sGAAsG;AACtG,2DAIC;AACD;;;GAGG;AACH;;;;;;;SAkBC;AAED,2FAA2F;AAC3F,oGAIC;AACD,gFA4BC;AAED,2GAA2G;AAC3G;;;EASC;AACD,mEAoBC;AACD,mEAGC;AACD,sEAUC;AACD,+EAEC;AAoCD,qFAYC;AACD,oGAqCC;AACD;;;SAKC;AACD,qEAGC;AACD,gEAgBC;AACD;;uGAEuG;AACvG,sEAGC;AACD,2DAQC;AACD,oGAUC;AAED,uEAAuE;AACvE,wHA+BC;AAED,uHAAuH;AACvH,6EAaC;AACD,mHAAmH;AACnH,iGAsDC;AACD;;;GAGG;AACH;;;SAcC;AACD,+EASC;AACD,8EAeC;AA7oBM,8CAAmE"}
@@ -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.must_complete !== undefined && typeof input.must_complete !== 'boolean')
53
+ throw new Error('FORM must_complete must be true or false');
52
54
  if (input.max_misses !== undefined && (!Number.isInteger(input.max_misses) || input.max_misses < 1))
53
55
  throw new Error('FORM max_misses must be a positive integer');
54
56
  if (input.max_attempts !== undefined && (!Number.isInteger(input.max_attempts) || input.max_attempts < 1))
@@ -285,7 +287,7 @@ export function settleFormActivations(batch = {}, cargo = null) {
285
287
  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) };
286
288
  }
287
289
  /** Explicit ownership: unrelated tasks and sibling inputs never share an instance by definition id. */
288
- 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 } = {}) {
290
+ 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 } = {}) {
289
291
  const incomplete = validateFormIncompleteOutcome(incomplete_outcome);
290
292
  validateFormExhaustion(on_exhausted);
291
293
  cargo.forms ||= {};
@@ -301,6 +303,8 @@ export function openFormInstance(cargo, definition, specs, { parent = null, inpu
301
303
  existing.incomplete_outcome = incomplete;
302
304
  if (on_exhausted !== undefined)
303
305
  existing.on_exhausted = on_exhausted;
306
+ if (must_complete !== undefined)
307
+ existing.must_complete = must_complete === true;
304
308
  return resume;
305
309
  }
306
310
  if (parent && !cargo.forms[parent])
@@ -317,13 +321,15 @@ export function openFormInstance(cargo, definition, specs, { parent = null, inpu
317
321
  cargo.forms[child].incomplete_outcome = incomplete;
318
322
  if (on_exhausted !== undefined)
319
323
  cargo.forms[child].on_exhausted = on_exhausted;
324
+ if (must_complete !== undefined)
325
+ cargo.forms[child].must_complete = must_complete === true;
320
326
  return child;
321
327
  }
322
328
  }
323
329
  cargo.formSequence = Number(cargo.formSequence || 0) + 1;
324
330
  const id = `${definition}:${cargo.formSequence}`;
325
331
  cargo.forms[id] = { id, definition, task: owner?.task || task || id, parent, result_input: input,
326
- on_exhausted: on_exhausted || '', 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,
332
+ 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,
327
333
  providers: providers || owner?.providers || null, questions: questions?.length ? questions : owner?.questions || [], status: 'collecting' };
328
334
  if (owner) {
329
335
  if (context) {
@@ -708,6 +714,18 @@ export function pendingFormReadings(cargo, id) {
708
714
  }
709
715
  return orderFormReadings(cargo, out);
710
716
  }
717
+ /** The caller answered: the runaway guard (replays > formWorkLimit) counts passes WITHOUT an answer, which is what an
718
+ * internal loop is, so an answer resets it on the answering form and every ancestor. Counting answers too ended a form
719
+ * whose caller simply needed several tries (a must_complete field re-asks every unreadable answer). */
720
+ export function answerResetsFormReplays(cargo, id) {
721
+ const seen = new Set();
722
+ while (id && cargo.forms?.[id] && !seen.has(id)) {
723
+ seen.add(id);
724
+ cargo.forms[id].replays = 0;
725
+ id = cargo.forms[id].parent;
726
+ }
727
+ return true;
728
+ }
711
729
  export function formWorkLimit(cargo, id) {
712
730
  const ancestors = new Set();
713
731
  while (cargo.forms?.[id]?.parent && !ancestors.has(id)) {
@@ -19,6 +19,7 @@ export namespace STANDARD_FUNCTIONS {
19
19
  export { formProposalDescriptor };
20
20
  export { clearFormMisses };
21
21
  export { missFormInput };
22
+ export { formMustComplete };
22
23
  export { formInputPrompt };
23
24
  export { consumeFormFeedback };
24
25
  export { formConversationOutcome };
@@ -102,6 +103,7 @@ export namespace STANDARD_FUNCTIONS {
102
103
  export { queueFormReadings };
103
104
  export { pendingFormReadings };
104
105
  export { formWorkLimit };
106
+ export { answerResetsFormReplays };
105
107
  export { rejectFormReadings };
106
108
  export { invalidateFormInputs };
107
109
  export { acceptFormReadings };
@@ -134,6 +136,7 @@ import { validateFormProposalDescriptor } from './proposal-contract.js';
134
136
  import { formProposalDescriptor } from './proposal-contract.js';
135
137
  import { clearFormMisses } from './form-conversation.js';
136
138
  import { missFormInput } from './form-conversation.js';
139
+ import { formMustComplete } from './form-conversation.js';
137
140
  import { formInputPrompt } from './form-conversation.js';
138
141
  import { consumeFormFeedback } from './form-conversation.js';
139
142
  import { formConversationOutcome } from './form-conversation.js';
@@ -219,6 +222,7 @@ import { formPreparation } from './form-state.js';
219
222
  import { queueFormReadings } from './form-state.js';
220
223
  import { pendingFormReadings } from './form-state.js';
221
224
  import { formWorkLimit } from './form-state.js';
225
+ import { answerResetsFormReplays } from './form-state.js';
222
226
  import { rejectFormReadings } from './form-state.js';
223
227
  import { invalidateFormInputs } from './form-state.js';
224
228
  import { acceptFormReadings } from './form-state.js';
@@ -234,5 +238,5 @@ import { validatePhone } from './validators.js';
234
238
  import { validateEmail } from './validators.js';
235
239
  import { urlToSpeech } from './url-speech.js';
236
240
  import { textWithUrlToSpeech } from './url-speech.js';
237
- export { continuationSafetyProtocol, cancelFormTask, contextualResponseProtocol, validateFormResponse, hasFormResponse, formProposalPolicy, stageFormProposal, nextFormProposal, formProposalReadings, bindFormProposal, decideFormProposal, commitFormProposals, settleFormProposalValidation, formProposalProtocol, validateFormProposalPolicy, validateFormProposalDescriptor, formProposalDescriptor, clearFormMisses, missFormInput, formInputPrompt, consumeFormFeedback, formConversationOutcome, readMoneyAmount, isMoneyAmount, moneyIntentAllows, isPastPaymentStatement, isPartialMoneyAnswer, validateMoneyOptions, takeChoiceOffer, matchesChoice, ANSWER_WORDS, CONFIRM_WORDS, isAnswer, isAgentRequest, isOtherRequest, isSpelledFragment, isAmountAnswer, amountInAnswer, pickChoice, readEmail, readPhone, readInternationalPhone, readIdentifier, last4, maskEmail, callerTextNumber, textDestination, callerIsInternational, normalizeAndFindCapture, anythingElse, goodbye, sayName, dateToWords, amountToSpeech, validateCardNumber, validateExpiration, consumeActionReceipt, authorizeFormActionRetry, formSignature, selectFormAction, confirmFormRevision, prepareFormAction, actionConfirmationValid, beginFormAction, finishFormAction, actionGateVerified, sourceNeedsLoad, beginSourceRefresh, completeSourceRefresh, pickProviderQuestion, matchInputs, readFormAnswer, readInputProfile, formAcceptanceTrace, resolveFormInputs, formInputApplies, formInputFilled, nextFormInput, formInputChain, openFormInstance, validateFormIncompleteOutcome, formIncompleteOutcome, validateFormExhaustion, exhaustFormInput, formInitialReadings, orderFormReadings, proposedFormValues, applyFormValidation, formReadingDependencies, exclusiveFormReadings, enrichFormMatches, expandFormActivations, settleFormActivations, rememberFormArguments, resetFormInputs, hasFormValue, formPath, formValuesEqual, formReadingReady, formReadingRelevant, formPreparation, queueFormReadings, pendingFormReadings, formWorkLimit, rejectFormReadings, invalidateFormInputs, acceptFormReadings, formInstanceApplies, formParentResume, closeFormInstance, checkOtp, maskDestination, validateOTP, OTP_EXPIRY_MS, validateDigits, validatePhone, validateEmail, urlToSpeech, textWithUrlToSpeech };
241
+ 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, 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 };
238
242
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/helpers/index.js"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;2CACyC,iCAAiC;+BAI6C,wBAAwB;2CAHjE,0BAA0B;qCAA1B,0BAA0B;gCAA1B,0BAA0B;mCACsE,qBAAqB;kCAArB,qBAAqB;iCAArB,qBAAqB;qCAArB,qBAAqB;iCAArB,qBAAqB;mCAArB,qBAAqB;oCAArB,qBAAqB;6CAArB,qBAAqB;qCAC/E,wBAAwB;2CAAxB,wBAAwB;+CAAxB,wBAAwB;uCAAxB,wBAAwB;gCACrB,wBAAwB;8BAAxB,wBAAwB;gCAAxB,wBAAwB;oCAAxB,wBAAwB;wCAAxB,wBAAwB;gCAmBhB,YAAY;8BAAZ,YAAY;kCAAZ,YAAY;uCAAZ,YAAY;qCAAZ,YAAY;qCAAZ,YAAY;gCAlB3G,mBAAmB;8BAiBrB,qBAAqB;6BAEkG,aAAa;8BAAb,aAAa;yBAAb,aAAa;+BAAb,aAAa;+BAAb,aAAa;kCAAb,aAAa;+BAAb,aAAa;+BAAb,aAAa;2BAAb,aAAa;0BACT,cAAc;0BAAd,cAAc;uCAAd,cAAc;+BAAd,cAAc;sBAAd,cAAc;0BAAd,cAAc;iCAAd,cAAc;gCAAd,cAAc;sCAAd,cAAc;wCAC/H,cAAc;6BACP,cAAc;wBAAd,cAAc;wBAAd,cAAc;4BACjC,iBAAiB;+BACd,uBAAuB;mCACC,kBAAkB;mCAAlB,kBAAkB;qCAxB+I,mBAAmB;yCAAnB,mBAAmB;8BAAnB,mBAAmB;iCAAnB,mBAAmB;oCAAnB,mBAAmB;kCAAnB,mBAAmB;wCAAnB,mBAAmB;gCAAnB,mBAAmB;iCAAnB,mBAAmB;mCAAnB,mBAAmB;gCAe1I,mBAAmB;mCAAnB,mBAAmB;sCAAnB,mBAAmB;qCAAnB,mBAAmB;4BAUjC,mBAAmB;+BAAnB,mBAAmB;iCAAnB,mBAAmB;oCAAnB,mBAAmB;kCAC0mB,iBAAiB;iCAAjB,iBAAiB;gCAAjB,iBAAiB;8BAAjB,iBAAiB;+BAAjB,iBAAiB;iCAAjB,iBAAiB;8CAAjB,iBAAiB;sCAAjB,iBAAiB;uCAAjB,iBAAiB;iCAAjB,iBAAiB;oCAAjB,iBAAiB;kCAAjB,iBAAiB;mCAAjB,iBAAiB;oCAAjB,iBAAiB;wCAAjB,iBAAiB;sCAAjB,iBAAiB;kCAAjB,iBAAiB;sCAAjB,iBAAiB;sCAAjB,iBAAiB;sCAAjB,iBAAiB;gCAAjB,iBAAiB;6BAAjB,iBAAiB;yBAAjB,iBAAiB;gCAAjB,iBAAiB;iCAAjB,iBAAiB;oCAAjB,iBAAiB;gCAAjB,iBAAiB;kCAAjB,iBAAiB;oCAAjB,iBAAiB;8BAAjB,iBAAiB;mCAAjB,iBAAiB;qCAAjB,iBAAiB;mCAAjB,iBAAiB;oCAAjB,iBAAiB;iCAAjB,iBAAiB;kCAAjB,iBAAiB;yBAC3pB,gBAAgB;gCAAhB,gBAAgB;4BAAhB,gBAAgB;8BAAhB,gBAAgB;+BACzB,iBAAiB;8BAAjB,iBAAiB;8BAAjB,iBAAiB;4BAC7B,iBAAiB;oCAAjB,iBAAiB"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/helpers/index.js"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;2CACyC,iCAAiC;+BAI8D,wBAAwB;2CAHlF,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;gCACJ,wBAAwB;8BAAxB,wBAAwB;iCAAxB,wBAAwB;gCAAxB,wBAAwB;oCAAxB,wBAAwB;wCAAxB,wBAAwB;gCAmBjC,YAAY;8BAAZ,YAAY;kCAAZ,YAAY;uCAAZ,YAAY;qCAAZ,YAAY;qCAAZ,YAAY;gCAlB3G,mBAAmB;8BAiBrB,qBAAqB;6BAEkG,aAAa;8BAAb,aAAa;yBAAb,aAAa;+BAAb,aAAa;+BAAb,aAAa;kCAAb,aAAa;+BAAb,aAAa;+BAAb,aAAa;2BAAb,aAAa;0BACT,cAAc;0BAAd,cAAc;uCAAd,cAAc;+BAAd,cAAc;sBAAd,cAAc;0BAAd,cAAc;iCAAd,cAAc;gCAAd,cAAc;sCAAd,cAAc;wCAC/H,cAAc;6BACP,cAAc;wBAAd,cAAc;wBAAd,cAAc;4BACjC,iBAAiB;+BACd,uBAAuB;mCACC,kBAAkB;mCAAlB,kBAAkB;qCAxB+I,mBAAmB;yCAAnB,mBAAmB;8BAAnB,mBAAmB;iCAAnB,mBAAmB;oCAAnB,mBAAmB;kCAAnB,mBAAmB;wCAAnB,mBAAmB;gCAAnB,mBAAmB;iCAAnB,mBAAmB;mCAAnB,mBAAmB;gCAe1I,mBAAmB;mCAAnB,mBAAmB;sCAAnB,mBAAmB;qCAAnB,mBAAmB;4BAUjC,mBAAmB;+BAAnB,mBAAmB;iCAAnB,mBAAmB;oCAAnB,mBAAmB;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"}
@@ -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, formInputPrompt, consumeFormFeedback, formConversationOutcome } from './form-conversation.js';
6
+ import { cancelFormTask, clearFormMisses, 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
  /**
@@ -31,18 +31,18 @@ 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
33
  import { matchInputs, readFormAnswer, readInputProfile, formAcceptanceTrace } from './match-inputs.js';
34
- import { resolveFormInputs, formInputApplies, formInputFilled, nextFormInput, formInputChain, openFormInstance, validateFormIncompleteOutcome, formIncompleteOutcome, validateFormExhaustion, exhaustFormInput, formInitialReadings, orderFormReadings, proposedFormValues, applyFormValidation, formReadingDependencies, exclusiveFormReadings, enrichFormMatches, expandFormActivations, settleFormActivations, rememberFormArguments, resetFormInputs, hasFormValue, formPath, formValuesEqual, formReadingReady, formReadingRelevant, formPreparation, queueFormReadings, pendingFormReadings, formWorkLimit, rejectFormReadings, invalidateFormInputs, acceptFormReadings, formInstanceApplies, formParentResume, closeFormInstance } from './form-state.js';
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, maskDestination, validateOTP, OTP_EXPIRY_MS } from './otp-check.js';
36
36
  import { validateDigits, validatePhone, validateEmail } from './validators.js';
37
37
  import { urlToSpeech, textWithUrlToSpeech } from './url-speech.js';
38
- export { continuationSafetyProtocol, cancelFormTask, contextualResponseProtocol, validateFormResponse, hasFormResponse, formProposalPolicy, stageFormProposal, nextFormProposal, formProposalReadings, bindFormProposal, decideFormProposal, commitFormProposals, settleFormProposalValidation, formProposalProtocol, validateFormProposalPolicy, validateFormProposalDescriptor, formProposalDescriptor, clearFormMisses, missFormInput, formInputPrompt, consumeFormFeedback, formConversationOutcome, readMoneyAmount, isMoneyAmount, moneyIntentAllows, isPastPaymentStatement, isPartialMoneyAnswer, validateMoneyOptions, takeChoiceOffer, matchesChoice, ANSWER_WORDS, CONFIRM_WORDS, isAnswer, isAgentRequest, isOtherRequest, isSpelledFragment, isAmountAnswer, amountInAnswer, pickChoice, readEmail, readPhone, readInternationalPhone, readIdentifier, last4, maskEmail, callerTextNumber, textDestination, callerIsInternational, normalizeAndFindCapture, anythingElse, goodbye, sayName, dateToWords, amountToSpeech, validateCardNumber, validateExpiration, consumeActionReceipt, authorizeFormActionRetry, formSignature, selectFormAction, confirmFormRevision, prepareFormAction, actionConfirmationValid, beginFormAction, finishFormAction, actionGateVerified, sourceNeedsLoad, beginSourceRefresh, completeSourceRefresh, pickProviderQuestion, matchInputs, readFormAnswer, readInputProfile, formAcceptanceTrace, resolveFormInputs, formInputApplies, formInputFilled, nextFormInput, formInputChain, openFormInstance, validateFormIncompleteOutcome, formIncompleteOutcome, validateFormExhaustion, exhaustFormInput, formInitialReadings, orderFormReadings, proposedFormValues, applyFormValidation, formReadingDependencies, exclusiveFormReadings, enrichFormMatches, expandFormActivations, settleFormActivations, rememberFormArguments, resetFormInputs, hasFormValue, formPath, formValuesEqual, formReadingReady, formReadingRelevant, formPreparation, queueFormReadings, pendingFormReadings, formWorkLimit, rejectFormReadings, invalidateFormInputs, acceptFormReadings, formInstanceApplies, formParentResume, closeFormInstance, checkOtp, maskDestination, validateOTP, OTP_EXPIRY_MS, validateDigits, validatePhone, validateEmail, urlToSpeech, textWithUrlToSpeech, };
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, 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, formInputPrompt, consumeFormFeedback, formConversationOutcome,
43
+ clearFormMisses, missFormInput, formMustComplete, formInputPrompt, consumeFormFeedback, formConversationOutcome,
44
44
  readMoneyAmount, isMoneyAmount, moneyIntentAllows, isPastPaymentStatement, isPartialMoneyAnswer, validateMoneyOptions, takeChoiceOffer, matchesChoice, isAnswer, isAgentRequest, isOtherRequest, isSpelledFragment, isAmountAnswer, amountInAnswer, pickChoice,
45
45
  readEmail, readPhone, readInternationalPhone, readIdentifier, last4, maskEmail, callerTextNumber, textDestination, callerIsInternational,
46
- normalizeAndFindCapture, anythingElse, goodbye, sayName, dateToWords, amountToSpeech, validateCardNumber, validateExpiration, consumeActionReceipt, authorizeFormActionRetry, formSignature, selectFormAction, confirmFormRevision, prepareFormAction, actionConfirmationValid, beginFormAction, finishFormAction, actionGateVerified, sourceNeedsLoad, beginSourceRefresh, completeSourceRefresh, pickProviderQuestion, matchInputs, readFormAnswer, readInputProfile, formAcceptanceTrace, resolveFormInputs, formInputApplies, formInputFilled, nextFormInput, formInputChain, openFormInstance, validateFormIncompleteOutcome, formIncompleteOutcome, validateFormExhaustion, exhaustFormInput, formInitialReadings, orderFormReadings, proposedFormValues, applyFormValidation, formReadingDependencies, exclusiveFormReadings, enrichFormMatches, expandFormActivations, settleFormActivations, rememberFormArguments, resetFormInputs, hasFormValue, formPath, formValuesEqual, formReadingReady, formReadingRelevant, formPreparation, queueFormReadings, pendingFormReadings, formWorkLimit, rejectFormReadings, invalidateFormInputs, acceptFormReadings, formInstanceApplies, formParentResume, closeFormInstance,
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,
47
47
  validateOTP, validateDigits, validatePhone, validateEmail, urlToSpeech, textWithUrlToSpeech,
48
48
  };
@@ -1 +1 @@
1
- export const JSFE_RUNTIME_FINGERPRINT = "sha256:f1c20de527015068a3315bbb280a647bf9ee7025b781e52500e27ac7677e3787";
1
+ export const JSFE_RUNTIME_FINGERPRINT = "sha256:5dcc5703f78eb0d76716ae4ec5d702abefd508aac9a44b3cb67b62efc93e244d";
@@ -248,6 +248,8 @@ The loop skips accepted data and asks the first ready missing required field. Co
248
248
 
249
249
  `max_misses` defaults to two consecutive unreadable answers: clarify the first with explicit retry text or a localized “Sorry, I didn't catch that.” prefix; yield the second utterance to DISPATCH while preserving the draft. Deterministic uncertainty may yield earlier, especially unrecognized monetary instructions. Validator failures use a separate `max_attempts` budget. An out-of-order accepted answer is not a miss. On exhaustion the nearest declared `on_exhausted` receives `{form,input,failed_form,failed_input,reason:"attempts_exhausted"}` and must deliberately recover that branch; it does not receive consent.
250
250
 
251
+ `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.
252
+
251
253
  Use `response:{description}` for open descriptions that need the existing intent/extraction model's interpretation. The prepared question and field description become contextual DISPATCH context. An extracted answer still passes deterministic validation; missing usable data yields with progress saved. A supported different intent routes to that flow; otherwise the host can answer with its full context. No separate field classifier/model role is needed. Do not attempt exhaustive conversational inference with keyword lists.
252
254
 
253
255
  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):
@@ -1,3 +1,17 @@
1
+ ## 1.10.5: a key named before it
2
+
3
+ At a numbered choice, a word naming the key said right before it picks the key: "el número 4", "opción 2", "number 4", "the
4
+ 4" (`keyWithOnlyCourtesy`; prod 2026-10-25 18:25, "Número 4" and "El número 4" at a numbered product list were not
5
+ understood, twice, and the caller lost the flow). A number anywhere else in a sentence is still not a key.
6
+
7
+ ## 1.10.4: forms that must complete
8
+
9
+ `must_complete:true` on a form or a field (a field inherits it from any enclosing form; `false` on a field opts out): an
10
+ unreadable answer, long or short, a confirmation miss and an unresolved ambiguity are asked again instead of yielding to
11
+ DISPATCH (reference, "Retry, yield and recovery"). Prod 2026-10-25: "el número postal el código postal" at the billing-ZIP
12
+ correction ended the payment. The form runaway guard now counts passes since the caller's last answer, so a caller who
13
+ needs several tries is not mistaken for a loop.
14
+
1
15
  ## 1.10.3: a field is ready before an unnamed answer fills it
2
16
 
3
17
  A bare number never fills, out of order, a field whose `depends_on` prerequisites are not filled yet (`formInputChain`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jsfe",
3
- "version": "1.10.3",
3
+ "version": "1.10.5",
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",