jsfe 1.10.2 → 1.10.4

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":"AAqHA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA2DC;AACD;;;GAGG;AACH;;;;;;EAGC;AAyYD,wHAAwH;AACxH,wEAaC;AACD,4FAA4F;AAC5F;;;EAYC;AACD,sGAAsG;AACtG;;;EAOC;AA5jBM,6CAAuG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmvB9G;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;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"}
@@ -70,7 +70,8 @@ export const FIELD_PRESETS = {
70
70
  question: 'Please say or enter your card number.', question_es: 'Por favor diga o ingrese el número de su tarjeta.',
71
71
  retry: 'Please say or enter the full card number.', retry_es: 'Por favor diga o ingrese el número completo de su tarjeta.' },
72
72
  card_expiration: { label: 'expiration date', label_es: 'fecha de vencimiento',
73
- profile: { kind: 'pattern', regex: '\\b(0?[1-9]|1[0-2])\\s*(?:/|-|\\s|de)?\\s*(?:20)?(\\d{2})\\b' }, digits: { min: 3, max: 6 },
73
+ // MM0YY: a zero said between month and year ("020 28", prod 2026-10-25 14:10, was taken for the billing ZIP)
74
+ profile: { kind: 'pattern', regex: '\\b(0?[1-9]|1[0-2])\\s*(?:/|-|\\s|de)?\\s*(?:20)?(\\d{2})\\b|\\b0[1-9]0\\s+(?:20)?\\d{2}\\b' }, digits: { min: 3, max: 6 },
74
75
  validator: 'validate-card-expiration',
75
76
  question: 'What is the expiration date, month and year?', question_es: '¿Cuál es la fecha de vencimiento, mes y año?',
76
77
  retry: 'Please say or enter the expiration as four digits, month then year, like 0 9 2 8.', retry_es: 'Por favor diga o ingrese el vencimiento en cuatro dígitos, mes y año, por ejemplo 0 9 2 8.' },
@@ -111,8 +112,10 @@ function validateAcceptanceDeclarations(id, inputs) {
111
112
  else { /* no invites, or a list of names */ }
112
113
  }
113
114
  }
114
- export function formFlow({ id, name, version, description, inputs, after = [], extraVars = {}, parameters = [], confirm = null, before_confirm = [], declined = [], stop = '', close_on_stop = false, resume_on_parent_change = false, before = [], done = true, primary = false, scope = 'isolated', validator = '', confirmation_condition = 'true', prompt = undefined, prompt_es = undefined, incomplete_outcome = null, on_exhausted = null }) {
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 }) {
115
116
  validateFormExhaustion(on_exhausted);
117
+ if (typeof must_complete !== 'boolean')
118
+ throw new Error(`FORM ${id}: must_complete must be true or false`);
116
119
  validateAcceptanceDeclarations(id, inputs);
117
120
  const incompleteFrom = incomplete_outcome && Object.hasOwn(incomplete_outcome, 'from');
118
121
  if (incompleteFrom) {
@@ -136,13 +139,13 @@ export function formFlow({ id, name, version, description, inputs, after = [], e
136
139
  ] : [];
137
140
  return {
138
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 } : {}),
139
- 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 }),
140
143
  steps: [
141
144
  ...contextualResponseRuntimeSteps(id, [], hasFormResponse(inputs) ? [CONTEXTUAL_RESPONSES_REQUIREMENT] : []),
142
145
  ...(hasFormResponse(inputs) ? [{ ...SET(`${id}_response_reader_required`, 'noop', "typeof validateFormResponse === 'function'"), requireTrue: true }] : []),
143
146
  ...proposalRuntimeSteps(id, [], hasInputProposals(inputs) ? [INPUT_PROPOSALS_REQUIREMENT] : []),
144
147
  SET(`${id}_resume_kind`, 'form_internal_resume', '!!form_instance'),
145
- 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 })`),
146
149
  SIDE(`${id}_task`, `(!parent_form ? (cargo.formTasks = cargo.formTasks || {}, cargo.formTasks[${JSON.stringify(id)}] = form_instance) : true, true)`),
147
150
  SIDE(`${id}_provider_reset`, '(delete cargo.forms[form_instance].providerRequests, delete cargo.forms[form_instance].activeGates, delete cargo.forms[form_instance].providerGateActive, true)'),
148
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)'),
@@ -232,7 +235,7 @@ const collectInput = {
232
235
  id: 'collect-input', name: 'CollectInput', version: '1.0.0', scope: 'isolated',
233
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).",
234
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 })),
235
- 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: '' }),
236
239
  steps: [
237
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; })()"),
238
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]"),
@@ -241,7 +244,9 @@ const collectInput = {
241
244
  SET('ci_max_attempts', 'ci_max', 'Number(ci_spec.max_attempts || 3)'),
242
245
  SET('ci_context_misses', 'ci_misses', 'Number(((cargo.forms[form] || {}).misses || {})[current] || 0)'),
243
246
  SET('ci_context_limit', 'ci_miss_limit', 'Number(ci_spec.max_misses || 2)'),
244
- 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'")),
245
250
  SET('ci_context', 'ci_ctx', "typeof exit_context !== 'undefined' && exit_context ? exit_context : 'your request'"),
246
251
  SET('ci_context_es', 'ci_ctx_es', "typeof exit_context_es !== 'undefined' && exit_context_es ? exit_context_es : 'su solicitud'"),
247
252
  WHEN('ci_resolve_exhausted', 'ci_missed >= ci_max', SET('ci_exhausted_policy', 'ci_exhaustion', 'exhaustFormInput(cargo,form,current)')),
@@ -268,6 +273,7 @@ const collectInput = {
268
273
  SET('ci_validator', 'ci_val', "ci_spec.answer_validator || ''"),
269
274
  SET('ci_reader_context', 'ci_reader', '({form:form,current:current,names:ci_names,questions:provides && Array.isArray(questions)?questions:[]})'),
270
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)'),
271
277
  // a key the input routes to a handler of its own (route_keys: [{ prefix, flow }]): that flow takes the answer
272
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] || ''"),
273
279
  WHEN('ci_route_key', 'ci_routed', FLOW('ci_route_key_flow', '{{ci_routed}}', 'call', { form: '{{form}}', input: '{{current}}' })),
@@ -295,7 +301,7 @@ const collectInput = {
295
301
  'condition: ci_m.hits.length > 0': FLOW('ci_accept', 'collect-input-accept', 'call', { hits: '{{ci_m.hits}}' }),
296
302
  "condition: ci_m.questions.length > 0": END('ci_question_handled'),
297
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}}' }),
298
- "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'"),
299
305
  default: SIDE('ci_miss', "(missFormInput(cargo,form,current),true)")
300
306
  } },
301
307
  ],
@@ -415,7 +421,7 @@ const disambiguate = {
415
421
  id: 'disambiguate-input', name: 'DisambiguateInput', version: '1.0.0', scope: 'isolated',
416
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.",
417
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' }],
418
- 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: [] }),
419
425
  steps: [
420
426
  WHEN('di_store_clear', 'Array.isArray(hits) && hits.length > 0', FLOW('di_store_hits', 'collect-input-accept', 'call', { hits: '{{hits}}' })),
421
427
  SET('di_number', 'di_keys', `${KEYS_JS}(readings.length)`),
@@ -425,17 +431,19 @@ const disambiguate = {
425
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(', ') + '.'"),
426
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(', ') + '.'"),
427
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);})'),
428
435
  SET('di_counter', 'di_input', "'__ambiguity:'+readings.map(function(r){return r.form+':'+r.name;}).sort().join('|')"),
429
436
  SET('di_prompt', 'di_prompt', "formInputPrompt(cargo,di_form,di_input,{question:di_q,question_es:di_q_es},{},'en')"),
430
437
  SET('di_prompt_es', 'di_prompt_es', "formInputPrompt(cargo,di_form,di_input,{question:di_q,question_es:di_q_es},{},'es')"),
431
438
  SIDE('di_feedback_consumed', 'consumeFormFeedback(cargo,di_form,di_input)'),
432
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)'),
433
441
  SET('di_picked', 'di_pick', "/^r\\d+$/.test(String(cargo.choice || '')) ? [readings[Number(String(cargo.choice).slice(1))]] : null"),
434
442
  WHEN('di_understood', 'di_pick', SIDE('di_clear_misses', 'clearFormMisses(cargo,di_form,di_input)')),
435
443
  SET('di_word_count', 'di_words', "String(cargo.choiceText || '').split(/\\s+/).filter(function(w){return /[a-záéíóúüñ]/i.test(w);}).length"),
436
- 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'")),
437
445
  WHEN('di_miss', '!di_pick', SET('di_miss_count', 'di_misses', 'missFormInput(cargo,di_form,di_input)')),
438
- 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'")),
439
447
  WHEN('di_retry', '!di_pick', FLOW('di_retry_question', 'disambiguate-input', 'replace', { readings: '{{readings}}', hits: [], remaining: '{{remaining}}' })),
440
448
  WHEN('di_store_pick', 'di_pick', FLOW('di_store_pick_flow', 'collect-input-accept', 'call', { hits: '{{di_pick}}' })),
441
449
  SET('di_remaining_first', 'di_next', 'Array.isArray(remaining) && remaining.length ? remaining[0] : []'),
@@ -448,7 +456,7 @@ const confirmForm = {
448
456
  id: 'confirm-form', name: 'ConfirmForm', version: '1.0.0', scope: 'isolated',
449
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).",
450
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" }]),
451
- variables: vars({ named_fields: false, cf_named: null, cf_presets: FIELD_PRESETS, cf_chain: [], cf_m: null, cf_current: null, cf_result: '', cf_prompt: '', cf_prompt_es: '', cf_words: 0, cf_tries: 0, phrases: null, words: [], clarifications: [], no_keys: ['2'], interrupts: [], invites: [], cf_choices: [], cf_event: '', cf_amount: false, cf_questions: [], cf_provides: null, cf_skip_gate: '' }),
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: '' }),
452
460
  steps: [
453
461
  SIDE('cf_reset', "(cargo.formConfirm = cargo.formConfirm || {}, cargo.formConfirm[form] = '', true)"),
454
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};}))`),
@@ -458,6 +466,7 @@ const confirmForm = {
458
466
  SET('cf_prompt_es', 'cf_prompt_es', "formInputPrompt(cargo,form,'__confirm',{question:question,question_es:question_es},{},'es')"),
459
467
  SIDE('cf_feedback_consumed', "consumeFormFeedback(cargo,form,'__confirm')"),
460
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)'),
461
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] || ''"),
462
471
  WHEN('cf_interrupt', 'cf_event', FLOW('cf_event_flow', '{{cf_event}}', 'reboot', { form: '{{form}}', event: '{{cargo.choice}}', text: '{{cargo.choiceText}}' })),
463
472
  SET('cf_build_chain', 'cf_chain', CHAIN_JS),
@@ -475,6 +484,7 @@ const confirmForm = {
475
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"),
476
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}}' })),
477
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')"),
478
488
  { id: 'cf_route', type: 'CASE', branches: {
479
489
  'condition: cf_m.questions.length > 0': SET('cf_question_repeat', 'cf_result', "'changed'"),
480
490
  "condition: cargo.choice === 'yes'": SET('cf_answered', 'cf_result', "'yes'"),
@@ -483,13 +493,13 @@ const confirmForm = {
483
493
  'condition: cf_named': SET('cf_named_reset', 'cf_result', "(resetFormInputs(cargo, cf_named.form, [cf_named.name]), 'changed')"),
484
494
  "condition: cargo.choice === 'no'": SET('cf_declined', 'cf_result', "'no'"),
485
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'"),
486
- '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'"),
487
497
  default: SET('cf_unclear', 'cf_result', "'unclear'")
488
498
  } },
489
499
  SET('cf_changed', 'cf_result', "cf_result ? cf_result : 'changed'"),
490
500
  // an unclear short answer is asked again (the form runs again and reaches this question), at most twice
491
501
  SET('cf_tries_count', 'cf_tries', "cf_result === 'unclear' ? missFormInput(cargo,form,'__confirm') : (clearFormMisses(cargo,form,'__confirm'),0)"),
492
- 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'")),
493
503
  WHEN('cf_bind_revision', "cf_result === 'yes'", SIDE('cf_bind', 'confirmFormRevision(cargo, form)')),
494
504
  SIDE('cf_store', "(cargo.formConfirm[form] = cf_result === 'unclear' || cf_result === 'clarification' ? 'changed' : cf_result, true)"),
495
505
  ],
@@ -537,7 +547,7 @@ const validators = [
537
547
  { id: 'validate-card-expiration', name: 'ValidateCardExpiration', version: '1.0.0', scope: 'isolated',
538
548
  description: 'Validator: month and year, a real month, not expired (validateExpiration). Normalizes to MMYY. Params: input, value.',
539
549
  parameters: [{ name: 'input', type: 'string', description: 'input name' }, { name: 'value', type: 'string', description: 'the reading' }],
540
- steps: [SIDE('vce_check', "(function (v) { var m = /^(0?[1-9]|1[0-2])\\D*(?:20)?(\\d{2})$/.exec(String(v).replace(/\\s+/g, '').replace(/de/gi, '')) || /^(\\d{2})(\\d{2})$/.exec(String(v).replace(/\\D/g, '')); var mm = m ? ('0' + m[1]).slice(-2) : ''; var yy = m ? m[2] : ''; cargo.inputCheck = m && validateExpiration(mm, yy) ? { ok: true, value: mm + yy } : { ok: false, message: \"That expiration date doesn't look right.\", message_es: 'Esa fecha de vencimiento no parece correcta.' }; return true; })(value)")] },
550
+ steps: [SIDE('vce_check', "(function (v) { var m = /^(0?[1-9]|1[0-2])\\D*(?:20)?(\\d{2})$/.exec(String(v).replace(/\\s+/g, '').replace(/de/gi, '')) || /^(\\d{2})(\\d{2})$/.exec(String(v).replace(/\\D/g, '')) || /^(0[1-9])0(\\d{2})$/.exec(String(v).replace(/\\D/g, '')); var mm = m ? ('0' + m[1]).slice(-2) : ''; var yy = m ? m[2] : ''; cargo.inputCheck = m && validateExpiration(mm, yy) ? { ok: true, value: mm + yy } : { ok: false, message: \"That expiration date doesn't look right.\", message_es: 'Esa fecha de vencimiento no parece correcta.' }; return true; })(value)")] },
541
551
  ];
542
552
  // ── field collectors and sub-forms ──────────────────────────────────────────────────────────────────────────────────
543
553
  const fieldCollectors = Object.entries(FIELD_COLLECTORS).map(([id, field]) => formFlow({
@@ -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,uEAuCC;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;AAhoBM,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))
@@ -190,7 +192,10 @@ export function formInputChain(forms, id, cargo = {}) {
190
192
  out.push({ ...input,
191
193
  ...(prefix ? { label: `${prefix}: ${input.label || input.name}`, label_es: `${prefixEs || prefix}: ${input.label_es || input.label || input.name}` } : {}),
192
194
  ...(input.choices_from && Array.isArray(state.data?.[input.choices_from]) ? { profile: { ...input.profile, choices: state.data[input.choices_from] } } : {}),
193
- ...(activation ? { activation } : {}), form: target, scope, ...(spec.any_of ? { group: spec.name, exclusive: spec.exclusive === true } : {}), value: state.values[input.name] });
195
+ ...(activation ? { activation } : {}), form: target, scope, ...(spec.any_of ? { group: spec.name, exclusive: spec.exclusive === true } : {}), value: state.values[input.name],
196
+ // its declared prerequisites hold accepted values: a field that cannot be validated yet is not a context for
197
+ // an unnamed out-of-order answer (contract, reference §5)
198
+ ready: (input.depends_on || spec.depends_on || []).every(name => hasFormValue(state.values[name])) });
194
199
  }
195
200
  }
196
201
  };
@@ -282,7 +287,7 @@ export function settleFormActivations(batch = {}, cargo = null) {
282
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) };
283
288
  }
284
289
  /** Explicit ownership: unrelated tasks and sibling inputs never share an instance by definition id. */
285
- 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 } = {}) {
286
291
  const incomplete = validateFormIncompleteOutcome(incomplete_outcome);
287
292
  validateFormExhaustion(on_exhausted);
288
293
  cargo.forms ||= {};
@@ -298,6 +303,8 @@ export function openFormInstance(cargo, definition, specs, { parent = null, inpu
298
303
  existing.incomplete_outcome = incomplete;
299
304
  if (on_exhausted !== undefined)
300
305
  existing.on_exhausted = on_exhausted;
306
+ if (must_complete !== undefined)
307
+ existing.must_complete = must_complete === true;
301
308
  return resume;
302
309
  }
303
310
  if (parent && !cargo.forms[parent])
@@ -314,13 +321,15 @@ export function openFormInstance(cargo, definition, specs, { parent = null, inpu
314
321
  cargo.forms[child].incomplete_outcome = incomplete;
315
322
  if (on_exhausted !== undefined)
316
323
  cargo.forms[child].on_exhausted = on_exhausted;
324
+ if (must_complete !== undefined)
325
+ cargo.forms[child].must_complete = must_complete === true;
317
326
  return child;
318
327
  }
319
328
  }
320
329
  cargo.formSequence = Number(cargo.formSequence || 0) + 1;
321
330
  const id = `${definition}:${cargo.formSequence}`;
322
331
  cargo.forms[id] = { id, definition, task: owner?.task || task || id, parent, result_input: input,
323
- 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,
324
333
  providers: providers || owner?.providers || null, questions: questions?.length ? questions : owner?.questions || [], status: 'collecting' };
325
334
  if (owner) {
326
335
  if (context) {
@@ -705,6 +714,18 @@ export function pendingFormReadings(cargo, id) {
705
714
  }
706
715
  return orderFormReadings(cargo, out);
707
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
+ }
708
729
  export function formWorkLimit(cargo, id) {
709
730
  const ancestors = new Set();
710
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
- {"version":3,"file":"match-inputs.d.ts","sourceRoot":"","sources":["../../src/helpers/match-inputs.js"],"names":[],"mappings":"AAiBA;;;;GAIG;AACH,sEAQC;AAqED;;;GAGG;AACH,sFAwGC;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH;mHACmH;AACnH;;;;;;;;;;;;;;;;;;;;;EAeC;AACD;;;;;;;;;;;;;;;;;;;;;;EAgLC"}
1
+ {"version":3,"file":"match-inputs.d.ts","sourceRoot":"","sources":["../../src/helpers/match-inputs.js"],"names":[],"mappings":"AAiBA;;;;GAIG;AACH,sEAQC;AAqED;;;GAGG;AACH,sFAwGC;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH;mHACmH;AACnH;;;;;;;;;;;;;;;;;;;;;EAeC;AACD;;;;;;;;;;;;;;;;;;;;;;EAoLC"}
@@ -353,6 +353,11 @@ export function matchInputs(answer, candidates, current, choiceKey) {
353
353
  else { /* the current input */ }
354
354
  return true;
355
355
  }
356
+ else if (bare(reading) && fieldOf(reading).ready === false) {
357
+ // a bare number for a field whose prerequisites are not filled yet (the sub-account before the account is known)
358
+ refused.push({ form: reading.form, name: reading.name, value: reading.value, rule: 'prerequisite_unmet', spans: reading.spans });
359
+ return false;
360
+ }
356
361
  else if (bare(reading) && fieldOf(reading).profile.kind === 'amount') {
357
362
  refused.push({ form: reading.form, name: reading.name, value: reading.value, rule: 'bare_amount_out_of_order', spans: reading.spans });
358
363
  return false;
@@ -1 +1 @@
1
- export const JSFE_RUNTIME_FINGERPRINT = "sha256:bf44b0f20dbe41d3ea672662bd6e580e65376ec6aa62c922af8ff037c2663006";
1
+ export const JSFE_RUNTIME_FINGERPRINT = "sha256:32e5bd91d0c35d60c3126334ae4d931bd988af19be6acd7fcdb9023794e61840";
@@ -216,7 +216,9 @@ Rules, in order:
216
216
  invites. A match is that answer.
217
217
  2. **A confirmation is answered only when it is asked.** Out of order it never makes sense, so no words, YES, NO or the
218
218
  option's own phrases, fill a confirmation that is not the current question.
219
- 3. **Out of order requires the whole signature.** A digit field takes a number only when the whole number fits its
219
+ 3. **Out of order requires a ready field and the whole signature.** A bare number never fills, out of order, a field whose
220
+ `depends_on` prerequisites are not filled yet (the sub-account before the account is known); a named answer ("50
221
+ dollars") still waits as a pending value. **The whole signature:** A digit field takes a number only when the whole number fits its
220
222
  length; a pattern only when it covers the whole number; a choice only its declared or prepared values. A fragment of
221
223
  a longer number (the last four of "88 91 7112") fills nothing.
222
224
  4. **A bare number** (no money mark, no word naming a field) is an amount only at a question that is, invites or is
@@ -246,6 +248,8 @@ The loop skips accepted data and asks the first ready missing required field. Co
246
248
 
247
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.
248
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
+
249
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.
250
254
 
251
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,18 @@
1
+ ## 1.10.4: forms that must complete
2
+
3
+ `must_complete:true` on a form or a field (a field inherits it from any enclosing form; `false` on a field opts out): an
4
+ unreadable answer, long or short, a confirmation miss and an unresolved ambiguity are asked again instead of yielding to
5
+ DISPATCH (reference, "Retry, yield and recovery"). Prod 2026-10-25: "el número postal el código postal" at the billing-ZIP
6
+ correction ended the payment. The form runaway guard now counts passes since the caller's last answer, so a caller who
7
+ needs several tries is not mistaken for a loop.
8
+
9
+ ## 1.10.3: a field is ready before an unnamed answer fills it
10
+
11
+ A bare number never fills, out of order, a field whose `depends_on` prerequisites are not filled yet (`formInputChain`
12
+ marks each candidate `ready`; refusal rule `prerequisite_unmet`): an account number keyed in pieces is no longer taken for
13
+ the payment's sub-account before the account is known. A named answer still waits as a pending value. The shared card
14
+ expiration also reads a zero said between month and year ("020 28" as 02/28).
15
+
1
16
  ## 1.10.2: "account number" at the contact question
2
17
 
3
18
  At the shared contact question (`get-cell-or-email`), a caller who names the account number without giving it yet ("número
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jsfe",
3
- "version": "1.10.2",
3
+ "version": "1.10.4",
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",